diff --git a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java index 50ef7a14a4..6485fcd975 100644 --- a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java +++ b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java @@ -68,13 +68,16 @@ import com.igalia.wolvic.telemetry.TelemetryService; import com.igalia.wolvic.ui.OffscreenDisplay; import com.igalia.wolvic.ui.adapters.Language; +import com.igalia.wolvic.ui.widgets.AbstractTabsBar; import com.igalia.wolvic.ui.widgets.AppServicesProvider; +import com.igalia.wolvic.ui.widgets.HorizontalTabsBar; import com.igalia.wolvic.ui.widgets.KeyboardWidget; import com.igalia.wolvic.ui.widgets.NavigationBarWidget; import com.igalia.wolvic.ui.widgets.RootWidget; import com.igalia.wolvic.ui.widgets.TrayWidget; import com.igalia.wolvic.ui.widgets.UISurfaceTextureRenderer; import com.igalia.wolvic.ui.widgets.UIWidget; +import com.igalia.wolvic.ui.widgets.VerticalTabsBar; import com.igalia.wolvic.ui.widgets.WebXRInterstitialWidget; import com.igalia.wolvic.ui.widgets.Widget; import com.igalia.wolvic.ui.widgets.WidgetManagerDelegate; @@ -103,6 +106,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; @@ -216,6 +220,7 @@ public void run() { RootWidget mRootWidget; KeyboardWidget mKeyboard; NavigationBarWidget mNavigationBar; + AbstractTabsBar mTabsBar; CrashDialogWidget mCrashDialog; TrayWidget mTray; WhatsNewWidget mWhatsNewWidget = null; @@ -454,9 +459,19 @@ public void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { mTray = new TrayWidget(this); mTray.addListeners(mWindows); mTray.setAddWindowVisible(mWindows.canOpenNewWindow()); + + // Create Tabs bar widget + if (mSettings.getTabsLocation() == SettingsStore.TABS_LOCATION_HORIZONTAL) { + mTabsBar = new HorizontalTabsBar(this, mWindows); + } else if (mSettings.getTabsLocation() == SettingsStore.TABS_LOCATION_VERTICAL) { + mTabsBar = new VerticalTabsBar(this, mWindows); + } else { + mTabsBar = null; + } + attachToWindow(mWindows.getFocusedWindow(), null); - addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mKeyboard, mTray, mWebXRInterstitial)); + addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mKeyboard, mTray, mTabsBar, mWebXRInterstitial)); // Create the platform plugin after widgets are created to be extra safe. mPlatformPlugin = createPlatformPlugin(this); @@ -472,10 +487,18 @@ private void attachToWindow(@NonNull WindowWidget aWindow, @Nullable WindowWidge mKeyboard.attachToWindow(aWindow); mTray.attachToWindow(aWindow); + if (mTabsBar != null) { + mTabsBar.attachToWindow(aWindow); + mWindows.adjustWindowOffsets(); + } + if (aPrevWindow != null) { updateWidget(mNavigationBar); updateWidget(mKeyboard); updateWidget(mTray); + if (mTabsBar != null) { + updateWidget(mTabsBar); + } } } @@ -732,14 +755,37 @@ public void onConfigurationChanged(Configuration newConfig) { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(getString(R.string.settings_key_voice_search_service))) { - initializeSpeechRecognizer(); - } else if (key.equals(getString(R.string.settings_key_head_lock))) { - boolean isHeadLockEnabled = SettingsStore.getInstance(this).isHeadLockEnabled(); - setHeadLockEnabled(isHeadLockEnabled); - if (!isHeadLockEnabled) - recenterUIYaw(WidgetManagerDelegate.YAW_TARGET_ALL); + if (Objects.equals(key, getString(R.string.settings_key_voice_search_service))) { + initializeSpeechRecognizer(); + } else if (Objects.equals(key, getString(R.string.settings_key_head_lock))) { + boolean isHeadLockEnabled = mSettings.isHeadLockEnabled(); + setHeadLockEnabled(isHeadLockEnabled); + if (!isHeadLockEnabled) + recenterUIYaw(WidgetManagerDelegate.YAW_TARGET_ALL); + } else if (Objects.equals(key, getString(R.string.settings_key_tabs_location))) { + // remove the previous widget + if (mTabsBar != null) { + removeWidget(mTabsBar); + mTabsBar.releaseWidget(); + } + + switch (mSettings.getTabsLocation()) { + case SettingsStore.TABS_LOCATION_HORIZONTAL: + mTabsBar = new HorizontalTabsBar(this, mWindows); + break; + case SettingsStore.TABS_LOCATION_VERTICAL: + mTabsBar = new VerticalTabsBar(this, mWindows); + break; + case SettingsStore.TABS_LOCATION_TRAY: + default: + mTabsBar = null; + return; } + addWidget(mTabsBar); + mTabsBar.attachToWindow(mWindows.getFocusedWindow()); + updateWidget(mTabsBar); + mWindows.adjustWindowOffsets(); + } } void loadFromIntent(final Intent intent) { diff --git a/app/src/common/shared/com/igalia/wolvic/browser/SettingsStore.java b/app/src/common/shared/com/igalia/wolvic/browser/SettingsStore.java index f6ac43ea7d..d239032fd1 100644 --- a/app/src/common/shared/com/igalia/wolvic/browser/SettingsStore.java +++ b/app/src/common/shared/com/igalia/wolvic/browser/SettingsStore.java @@ -69,6 +69,12 @@ SettingsStore getInstance(final @NonNull Context aContext) { public static final int INTERNAL = 0; public static final int EXTERNAL = 1; + @IntDef(value = { TABS_LOCATION_TRAY, TABS_LOCATION_HORIZONTAL, TABS_LOCATION_VERTICAL}) + public @interface TabsLocation {} + public static final int TABS_LOCATION_TRAY = 0; + public static final int TABS_LOCATION_HORIZONTAL = 1; + public static final int TABS_LOCATION_VERTICAL = 2; + private Context mContext; private SharedPreferences mPrefs; private SettingsViewModel mSettingsViewModel; @@ -143,6 +149,7 @@ public static WindowSizePreset fromValues(int width, int height) { public final static boolean AUDIO_ENABLED = BuildConfig.FLAVOR_backend == "chromium"; public final static boolean LATIN_AUTO_COMPLETE_ENABLED = false; public final static boolean WINDOW_MOVEMENT_DEFAULT = false; + public final static @TabsLocation int TABS_LOCATION_DEFAULT = TABS_LOCATION_TRAY; public final static float CYLINDER_DENSITY_ENABLED_DEFAULT = 4680.0f; public final static float HAPTIC_PULSE_DURATION_DEFAULT = 10.0f; public final static float HAPTIC_PULSE_INTENSITY_DEFAULT = 1.0f; @@ -391,6 +398,18 @@ public void setHeadLockEnabled(boolean isEnabled) { editor.apply(); } + @TabsLocation + public int getTabsLocation() { + return mPrefs.getInt( + mContext.getString(R.string.settings_key_tabs_location), TABS_LOCATION_DEFAULT); + } + + public void setTabsLocation(@TabsLocation int tabsLocation) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putInt(mContext.getString(R.string.settings_key_tabs_location), tabsLocation); + editor.commit(); + } + public boolean isEnvironmentOverrideEnabled() { return mPrefs.getBoolean( mContext.getString(R.string.settings_key_environment_override), ENV_OVERRIDE_DEFAULT); diff --git a/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java b/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java index 37a2f269d1..8dc0db430a 100644 --- a/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java +++ b/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java @@ -212,6 +212,9 @@ protected void shutdown() { if (mState.mSession != null) { setActive(false); suspend(); + } else { + // Notify listeners manually. + mSessionChangeListeners.forEach(listener -> listener.onSessionRemoved(mState.mId)); } if (mState.mParentId != null) { diff --git a/app/src/common/shared/com/igalia/wolvic/browser/engine/SessionStore.java b/app/src/common/shared/com/igalia/wolvic/browser/engine/SessionStore.java index 282dc72a59..6010e799d2 100644 --- a/app/src/common/shared/com/igalia/wolvic/browser/engine/SessionStore.java +++ b/app/src/common/shared/com/igalia/wolvic/browser/engine/SessionStore.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executor; @@ -84,9 +85,11 @@ public static SessionStore get() { private FxaWebChannelFeature mWebChannelsFeature; private Store.Subscription mStoreSubscription; private BrowserIconsHelper mBrowserIconsHelper; + private final LinkedHashSet mSessionChangeListeners; private SessionStore() { mSessions = new ArrayList<>(); + mSessionChangeListeners = new LinkedHashSet<>(); } public void initialize(Context context) { @@ -358,6 +361,10 @@ public Session getActiveSession() { return mActiveSession; } + public List getSessions(boolean aPrivateMode) { + return mSessions.stream().filter(session -> session.isPrivateMode() == aPrivateMode).collect(Collectors.toList()); + } + public ArrayList getSortedSessions(boolean aPrivateMode) { ArrayList result = new ArrayList<>(mSessions); result.removeIf(session -> session.isPrivateMode() != aPrivateMode); @@ -374,6 +381,14 @@ public void setPermissionDelegate(PermissionDelegate delegate) { mPermissionDelegate = delegate; } + public void addSessionChangeListener(SessionChangeListener listener) { + mSessionChangeListeners.add(listener); + } + + public void removeSessionChangeListener(SessionChangeListener listener) { + mSessionChangeListeners.remove(listener); + } + public BookmarksStore getBookmarkStore() { return mBookmarksStore; } @@ -514,21 +529,33 @@ public void removePermissionException(@NonNull String uri, @SitePermission.Categ @Override public void onSessionAdded(Session aSession) { ComponentsAdapter.get().addSession(aSession); + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onSessionAdded(aSession); + } } @Override public void onSessionOpened(Session aSession) { ComponentsAdapter.get().link(aSession); + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onSessionOpened(aSession); + } } @Override public void onSessionClosed(Session aSession) { ComponentsAdapter.get().unlink(aSession); + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onSessionClosed(aSession); + } } @Override public void onSessionRemoved(String aId) { ComponentsAdapter.get().removeSession(aId); + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onSessionRemoved(aId); + } } @Override @@ -536,6 +563,9 @@ public void onSessionStateChanged(Session aSession, boolean aActive) { if (aActive) { ComponentsAdapter.get().selectSession(aSession); } + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onSessionStateChanged(aSession, aActive); + } } @Override @@ -549,6 +579,9 @@ public void onCurrentSessionChange(WSession aOldSession, WSession aSession) { ComponentsAdapter.get().link(newSession); } + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onCurrentSessionChange(aOldSession, aSession); + } } @Override diff --git a/app/src/common/shared/com/igalia/wolvic/ui/adapters/TabsBarAdapter.java b/app/src/common/shared/com/igalia/wolvic/ui/adapters/TabsBarAdapter.java new file mode 100644 index 0000000000..97dfa990b6 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/adapters/TabsBarAdapter.java @@ -0,0 +1,90 @@ +package com.igalia.wolvic.ui.adapters; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.igalia.wolvic.R; +import com.igalia.wolvic.browser.engine.Session; +import com.igalia.wolvic.ui.views.TabsBarItem; +import com.igalia.wolvic.ui.widgets.TabDelegate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TabsBarAdapter extends RecyclerView.Adapter { + + public enum Orientation {HORIZONTAL, VERTICAL} + + private final TabDelegate mTabDelegate; + private final Orientation mOrientation; + private final List mTabs = new ArrayList<>(); + + static class ViewHolder extends RecyclerView.ViewHolder { + TabsBarItem mTabBarItem; + + ViewHolder(TabsBarItem v) { + super(v); + mTabBarItem = v; + } + } + + public TabsBarAdapter(@NonNull TabDelegate tabDelegate, Orientation orientation) { + mTabDelegate = tabDelegate; + mOrientation = orientation; + } + + @Override + public long getItemId(int position) { + return (position == 0) ? 0 : mTabs.get(position - 1).getId().hashCode(); + } + + public void updateTabs(List aTabs) { + mTabs.clear(); + mTabs.addAll(aTabs); + + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + @LayoutRes int layout; + if (mOrientation == Orientation.HORIZONTAL) { + layout = R.layout.tabs_bar_item_horizontal; + } else { + layout = R.layout.tabs_bar_item_vertical; + } + TabsBarItem view = (TabsBarItem) LayoutInflater.from(parent.getContext()).inflate(layout, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.mTabBarItem.setDelegate(mItemDelegate); + + Session session = mTabs.get(position); + holder.mTabBarItem.attachToSession(session); + } + + @Override + public int getItemCount() { + return mTabs.size(); + } + + private final TabsBarItem.Delegate mItemDelegate = new TabsBarItem.Delegate() { + @Override + public void onClick(TabsBarItem item) { + mTabDelegate.onTabSelect(item.getSession()); + } + + @Override + public void onClose(TabsBarItem item) { + mTabDelegate.onTabsClose(Collections.singletonList(item.getSession())); + } + }; +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/TrayViewModel.java b/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/TrayViewModel.java index 12b0934038..7f62a689b4 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/TrayViewModel.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/TrayViewModel.java @@ -21,6 +21,7 @@ public class TrayViewModel extends AndroidViewModel { private MediatorLiveData isVisible; private MutableLiveData time; private MutableLiveData pm; + private MutableLiveData tabsButtonInTray; private MutableLiveData wifiConnected; private MutableLiveData headsetIcon; private MutableLiveData headsetBatteryLevel; @@ -43,7 +44,7 @@ public TrayViewModel(@NonNull Application application) { isVisible.setValue(new ObservableBoolean(false)); time = new MutableLiveData<>(); pm = new MutableLiveData<>(); - pm = new MutableLiveData<>(); + tabsButtonInTray = new MutableLiveData<>(new ObservableBoolean(true)); wifiConnected = new MutableLiveData<>(new ObservableBoolean(true)); headsetIcon = new MutableLiveData<>(new ObservableInt(R.drawable.ic_icon_statusbar_headset_normal)); headsetBatteryLevel = new MutableLiveData<>(new ObservableInt(R.drawable.ic_icon_statusbar_indicator)); @@ -69,6 +70,7 @@ public void refresh() { isKeyboardVisible.setValue(isKeyboardVisible.getValue()); time.postValue(time.getValue()); pm.postValue(pm.getValue()); + tabsButtonInTray.postValue(tabsButtonInTray.getValue()); wifiConnected.postValue(wifiConnected.getValue()); headsetIcon.setValue(headsetIcon.getValue()); headsetBatteryLevel.setValue(headsetBatteryLevel.getValue()); @@ -127,6 +129,14 @@ public MutableLiveData getPm() { return pm; } + public void setTabsButtonInTray(boolean tabsButtonInTray) { + this.tabsButtonInTray.setValue(new ObservableBoolean(tabsButtonInTray)); + } + + public MutableLiveData getTabsButtonInTray() { + return tabsButtonInTray; + } + public void setWifiConnected(boolean connected) { this.wifiConnected.setValue(new ObservableBoolean(connected)); } diff --git a/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/WindowViewModel.java b/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/WindowViewModel.java index c44a285ed4..b171b3f605 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/WindowViewModel.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/viewmodel/WindowViewModel.java @@ -46,6 +46,9 @@ public class WindowViewModel extends AndroidViewModel { private MutableLiveData isKioskMode; private MutableLiveData isDesktopMode; private MediatorLiveData isTopBarVisible; + private MediatorLiveData isTabsBarVisible; + private MutableLiveData usesHorizontalTabsBar; + private MutableLiveData usesVerticalTabsBar; private MutableLiveData isResizeMode; private MutableLiveData isPrivateSession; private MediatorLiveData showClearButton; @@ -158,6 +161,17 @@ public WindowViewModel(Application application) { titleBarUrl.addSource(url, mTitleBarUrlObserver); titleBarUrl.setValue(""); + isTabsBarVisible = new MediatorLiveData<>(); + isTabsBarVisible.addSource(isActiveWindow, mIsTabsBarVisibleObserver); + isTabsBarVisible.addSource(isFullscreen, mIsTabsBarVisibleObserver); + isTabsBarVisible.addSource(isKioskMode, mIsTabsBarVisibleObserver); + isTabsBarVisible.addSource(isResizeMode, mIsTabsBarVisibleObserver); + isTabsBarVisible.addSource(isWindowVisible, mIsTabsBarVisibleObserver); + isTabsBarVisible.setValue(new ObservableBoolean(true)); + + usesHorizontalTabsBar = new MutableLiveData<>(new ObservableBoolean(false)); + usesVerticalTabsBar = new MutableLiveData<>(new ObservableBoolean(false)); + isInsecureVisible = new MediatorLiveData<>(); isInsecureVisible.addSource(isInsecure, mIsInsecureVisibleObserver); isInsecureVisible.addSource(isPrivateSession, mIsInsecureVisibleObserver); @@ -213,6 +227,18 @@ public void onChanged(ObservableBoolean o) { } }; + private Observer mIsTabsBarVisibleObserver = new Observer() { + @Override + public void onChanged(ObservableBoolean o) { + if (!isActiveWindow.getValue().get() || isFullscreen.getValue().get() || isKioskMode.getValue().get() || isResizeMode.getValue().get() || !isWindowVisible.getValue().get()) { + isTabsBarVisible.postValue(new ObservableBoolean(false)); + + } else { + isTabsBarVisible.postValue(new ObservableBoolean(true)); + } + } + }; + private Observer mShowClearButtonObserver = new Observer() { @Override public void onChanged(ObservableBoolean o) { @@ -520,6 +546,29 @@ public void setIsTopBarVisible(boolean isTopBarVisible) { this.isTopBarVisible.postValue(new ObservableBoolean(isTopBarVisible)); } + @NonNull + public MediatorLiveData getIsTabsBarVisible() { + return isTabsBarVisible; + } + + @NonNull + public MutableLiveData getUsesHorizontalTabsBar() { + return usesHorizontalTabsBar; + } + + public void setUsesHorizontalTabsBar(boolean usesHorizontalTabsBar) { + this.usesHorizontalTabsBar.postValue(new ObservableBoolean(usesHorizontalTabsBar)); + } + + @NonNull + public MutableLiveData getUsesVerticalTabsBar() { + return usesVerticalTabsBar; + } + + public void setUsesVerticalTabsBar(boolean usesVerticalTabsBar) { + this.usesVerticalTabsBar.postValue(new ObservableBoolean(usesVerticalTabsBar)); + } + @NonNull public MutableLiveData getIsResizeMode() { return isResizeMode; diff --git a/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java b/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java new file mode 100644 index 0000000000..a38022d5d7 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java @@ -0,0 +1,174 @@ +package com.igalia.wolvic.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModel; + +import com.igalia.wolvic.R; +import com.igalia.wolvic.browser.SessionChangeListener; +import com.igalia.wolvic.browser.api.WSession; +import com.igalia.wolvic.browser.engine.Session; +import com.igalia.wolvic.browser.engine.SessionStore; +import com.igalia.wolvic.ui.widgets.WidgetManagerDelegate; +import com.igalia.wolvic.ui.widgets.WindowWidget; +import com.igalia.wolvic.utils.UrlUtils; + +import java.util.Objects; + +import mozilla.components.browser.icons.IconRequest; + +public class TabsBarItem extends RelativeLayout implements WSession.ContentDelegate, WSession.NavigationDelegate, + SessionChangeListener { + + protected ViewGroup mTabDetailsView; + protected ImageView mFavicon; + protected TextView mSubtitle; + protected TextView mTitle; + protected Button mCloseButton; + protected Delegate mDelegate; + protected Session mSession; + protected ViewModel mViewModel; + + public interface Delegate { + void onClick(TabsBarItem aSender); + + void onClose(TabsBarItem aSender); + } + + public TabsBarItem(Context context) { + super(context); + } + + public TabsBarItem(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TabsBarItem(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mTabDetailsView = findViewById(R.id.tab_details); + + mCloseButton = findViewById(R.id.tab_close_button); + mCloseButton.setOnClickListener(v -> { + v.requestFocusFromTouch(); + if (mDelegate != null) { + mDelegate.onClose(this); + } + }); + + mFavicon = findViewById(R.id.tab_favicon); + mTitle = findViewById(R.id.tab_title); + mSubtitle = findViewById(R.id.tab_subtitle); + + this.setOnClickListener(mClickListener); + } + + private final OnClickListener mClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (mDelegate != null) { + mDelegate.onClick(TabsBarItem.this); + } + } + }; + + public void attachToSession(@Nullable Session aSession) { + if (mSession != null) { + mSession.removeContentListener(this); + mSession.removeNavigationListener(this); + mSession.removeSessionChangeListener(this); + } + + mSession = aSession; + if (mSession != null) { + mSession.addContentListener(this); + mSession.addNavigationListener(this); + mSession.addSessionChangeListener(this); + + mTitle.setText(mSession.getCurrentTitle()); + mSubtitle.setText(UrlUtils.stripProtocol(mSession.getCurrentUri())); + SessionStore.get().getBrowserIcons().loadIntoView( + mFavicon, mSession.getCurrentUri(), IconRequest.Size.DEFAULT); + + if (getContext() instanceof WidgetManagerDelegate) { + WidgetManagerDelegate widgetManager = (WidgetManagerDelegate) getContext(); + WindowWidget focusedWindow = widgetManager.getFocusedWindow(); + if (focusedWindow != null && focusedWindow.getSession() != null) { + setActive(mSession.isActive() && Objects.equals(mSession.getId(), focusedWindow.getSession().getId())); + } + } else { + setActive(mSession.isActive()); + } + } else { + // Null session + mTitle.setText(null); + mSubtitle.setText(null); + mFavicon.setImageDrawable(null); + } + } + + public Session getSession() { + return mSession; + } + + public void setDelegate(Delegate aDelegate) { + mDelegate = aDelegate; + } + + @Override + public void onTitleChange(@NonNull WSession session, @Nullable String title) { + if (mSession == null || mSession.getWSession() != session) { + return; + } + mTitle.setText(title); + } + + @Override + public void onLocationChange(@NonNull WSession session, @Nullable String url) { + if (mSession == null || mSession.getWSession() != session) { + return; + } + + if (url == null) { + mSubtitle.setText(null); + mFavicon.setImageDrawable(null); + } else { + mSubtitle.setText(UrlUtils.stripProtocol(mSession.getCurrentUri())); + SessionStore.get().getBrowserIcons().loadIntoView( + mFavicon, mSession.getCurrentUri(), IconRequest.Size.DEFAULT); + } + } + + @Override + public void onSessionStateChanged(Session aSession, boolean aActive) { + // TODO this should only apply to the session in the focused window + if (Objects.equals(mSession, aSession)) { + setActive(aActive); + } + } + + @Override + public void onCloseRequest(@NonNull WSession aSession) { + if (mSession.getWSession() == aSession) { + mDelegate.onClose(this); + } + } + + public void setActive(boolean isActive) { + setSelected(isActive); + } +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/AbstractTabsBar.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/AbstractTabsBar.java new file mode 100644 index 0000000000..4b338b74da --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/AbstractTabsBar.java @@ -0,0 +1,146 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableBoolean; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import com.igalia.wolvic.VRBrowserActivity; +import com.igalia.wolvic.browser.SessionChangeListener; +import com.igalia.wolvic.browser.api.WSession; +import com.igalia.wolvic.browser.engine.Session; +import com.igalia.wolvic.browser.engine.SessionStore; +import com.igalia.wolvic.ui.viewmodel.WindowViewModel; +import com.igalia.wolvic.utils.SystemUtils; + +public abstract class AbstractTabsBar extends UIWidget implements SessionChangeListener, WidgetManagerDelegate.UpdateListener { + + protected final String LOGTAG = SystemUtils.createLogtag(this.getClass()); + + protected boolean mPrivateMode; + protected WindowWidget mAttachedWindow; + protected WindowViewModel mWindowViewModel; + + public AbstractTabsBar(Context aContext) { + super(aContext); + + SessionStore.get().addSessionChangeListener(this); + } + + public abstract void updateWidgetPlacement(); + + @Override + public void attachToWindow(@NonNull WindowWidget window) { + if (mAttachedWindow == window) { + return; + } + detachFromWindow(); + mAttachedWindow = window; + + mPrivateMode = mAttachedWindow.getSession() != null && mAttachedWindow.getSession().isPrivateMode(); + mWidgetManager.addUpdateListener(this); + mWindowViewModel = new ViewModelProvider( + (VRBrowserActivity) getContext(), + ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) + .get(String.valueOf(mAttachedWindow.hashCode()), WindowViewModel.class); + mWindowViewModel.getIsTabsBarVisible().observe((VRBrowserActivity) getContext(), mIsTabsBarVisibleObserver); + + updateWidgetPlacement(); + refreshTabs(); + } + + @Override + public void detachFromWindow() { + if (mWindowViewModel != null) { + mWindowViewModel.getIsTabsBarVisible().removeObserver(mIsTabsBarVisibleObserver); + mWindowViewModel = null; + } + mAttachedWindow = null; + } + + Observer mIsTabsBarVisibleObserver = isTabsVisible -> { + if (isTabsVisible.get()) { + show(CLEAR_FOCUS); + } else { + hide(KEEP_WIDGET); + } + }; + + @Override + public void show(@ShowFlags int aShowFlags) { + updateWidgetPlacement(); + mWidgetPlacement.visible = true; + mWidgetManager.updateWidget(this); + } + + @Override + public void hide(@HideFlags int aHideFlag) { + mWidgetPlacement.visible = false; + mWidgetManager.updateWidget(this); + } + + @Override + public void releaseWidget() { + if (mWidgetManager != null) { + mWidgetManager.removeUpdateListener(this); + mWindowViewModel.getIsTabsBarVisible().removeObserver(mIsTabsBarVisibleObserver); + } + SessionStore.get().removeSessionChangeListener(this); + super.releaseWidget(); + } + + // WidgetManagerDelegate.UpdateListener + @Override + public void onWidgetUpdate(Widget aWidget) { + if (aWidget == mAttachedWindow && !mAttachedWindow.isResizing()) { + updateWidgetPlacement(); + } + } + + // TODO Use more fine-grained updates. + public abstract void refreshTabs(); + + // SessionChangeListener + + @Override + public void onSessionAdded(Session aSession) { + refreshTabs(); + } + + @Override + public void onSessionOpened(Session aSession) { + refreshTabs(); + } + + @Override + public void onSessionClosed(Session aSession) { + refreshTabs(); + } + + @Override + public void onSessionRemoved(String aId) { + refreshTabs(); + } + + @Override + public void onSessionStateChanged(Session aSession, boolean aActive) { + refreshTabs(); + } + + @Override + public void onCurrentSessionChange(WSession aOldSession, WSession aSession) { + refreshTabs(); + } + + @Override + public void onStackSession(Session aSession) { + refreshTabs(); + } + + @Override + public void onUnstackSession(Session aSession, Session aParent) { + refreshTabs(); + } +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/HorizontalTabsBar.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/HorizontalTabsBar.java new file mode 100644 index 0000000000..48b4c3c83a --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/HorizontalTabsBar.java @@ -0,0 +1,73 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; +import android.widget.Button; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.igalia.wolvic.R; +import com.igalia.wolvic.browser.SettingsStore; +import com.igalia.wolvic.browser.engine.SessionStore; +import com.igalia.wolvic.ui.adapters.TabsBarAdapter; + +public class HorizontalTabsBar extends AbstractTabsBar { + + protected Button mAddTabButton; + protected RecyclerView mTabsList; + protected LinearLayoutManager mLayoutManager; + protected TabsBarAdapter mAdapter; + protected final TabDelegate mTabDelegate; + + public HorizontalTabsBar(Context aContext, TabDelegate aDelegate) { + super(aContext); + mTabDelegate = aDelegate; + updateUI(); + } + + private void updateUI() { + removeAllViews(); + + inflate(getContext(), R.layout.tabs_bar_horizontal, this); + + mAddTabButton = findViewById(R.id.add_tab); + mAddTabButton.setOnClickListener(v -> mTabDelegate.onTabAdd()); + + mTabsList = findViewById(R.id.tabsRecyclerView); + mLayoutManager = new LinearLayoutManager(getContext()); + mLayoutManager.setOrientation(RecyclerView.HORIZONTAL); + mTabsList.setLayoutManager(mLayoutManager); + mAdapter = new TabsBarAdapter(mTabDelegate, TabsBarAdapter.Orientation.HORIZONTAL); + mTabsList.setAdapter(mAdapter); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + Context context = getContext(); + aPlacement.width = SettingsStore.getInstance(getContext()).getWindowWidth(); + aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.horizontal_tabs_bar_height); + aPlacement.worldWidth = aPlacement.width * WidgetPlacement.worldToDpRatio(context); + aPlacement.translationX = WidgetPlacement.dpDimension(context, R.dimen.top_bar_window_margin); + aPlacement.anchorX = 0.0f; + aPlacement.anchorY = 0.0f; + aPlacement.parentAnchorX = 0.0f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.parentAnchorGravity = WidgetPlacement.GRAVITY_DEFAULT; + aPlacement.translationZ = 0.0f; // WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + @Override + public void updateWidgetPlacement() { + if (mAttachedWindow == null) { + mWidgetPlacement.parentHandle = -1; + } else { + mWidgetPlacement.parentHandle = mAttachedWindow.getHandle(); + mWidgetPlacement.width = mAttachedWindow.getPlacement().width; + mWidgetPlacement.worldWidth = mAttachedWindow.getPlacement().worldWidth; + } + } + + public void refreshTabs() { + mAdapter.updateTabs(SessionStore.get().getSessions(mPrivateMode)); + } +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java new file mode 100644 index 0000000000..efc6f41834 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java @@ -0,0 +1,12 @@ +package com.igalia.wolvic.ui.widgets; + +import com.igalia.wolvic.browser.engine.Session; + +import java.util.List; + +public interface TabDelegate { + void onTabAdd(); + void onTabSelect(Session aTab); + void onTabsClose(List aTabs); + void onTabsBookmark(List aTabs); +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsWidget.java index b0c44d9219..f8e45430d2 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsWidget.java @@ -48,13 +48,6 @@ public class TabsWidget extends UIDialog { protected boolean mSelecting; protected ArrayList mSelectedTabs = new ArrayList<>(); - public interface TabDelegate { - void onTabSelect(Session aTab); - void onTabAdd(); - void onTabsClose(List aTabs); - void onTabsBookmark(List aTabs); - } - public TabsWidget(Context aContext) { super(aContext); mBitmapCache = BitmapCache.getInstance(aContext); diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TopBarWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TopBarWidget.java index 48756e03ed..b53fbdb8a3 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TopBarWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TopBarWidget.java @@ -77,6 +77,16 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.layer = false; } + private void adjustWindowPlacement(boolean isHorizontalTabsVisible) { + if (isHorizontalTabsVisible) { + // Move this widget upwards to make space for the horizontal tabs bar. + getPlacement().verticalOffset = WidgetPlacement.dpDimension(getContext(), R.dimen.horizontal_tabs_bar_height) * WidgetPlacement.worldToDpRatio(getContext()); + } else { + getPlacement().verticalOffset = 0; + } + mWidgetManager.updateWidget(TopBarWidget.this); + } + private void updateUI() { removeAllViews(); @@ -141,6 +151,8 @@ public void detachFromWindow() { if (mViewModel != null) { mViewModel.getIsTopBarVisible().removeObserver(mIsVisible); + mViewModel.getIsTabsBarVisible().removeObserver(mIsTabsBarVisible); + mViewModel.getUsesHorizontalTabsBar().removeObserver(mUsesHorizontalTabsBar); mViewModel = null; } } @@ -163,7 +175,9 @@ public void attachToWindow(@NonNull WindowWidget aWindow) { mBinding.setViewmodel(mViewModel); - mViewModel.getIsTopBarVisible().observe((VRBrowserActivity)getContext(), mIsVisible); + mViewModel.getIsTopBarVisible().observe((VRBrowserActivity) getContext(), mIsVisible); + mViewModel.getIsTabsBarVisible().observe((VRBrowserActivity) getContext(), mIsTabsBarVisible); + mViewModel.getUsesHorizontalTabsBar().observe((VRBrowserActivity) getContext(), mUsesHorizontalTabsBar); } public @Nullable WindowWidget getAttachedWindow() { @@ -178,7 +192,7 @@ public void releaseWidget() { super.releaseWidget(); } - Observer mIsVisible = isVisible -> { + private Observer mIsVisible = isVisible -> { mWidgetPlacement.visible = isVisible.get(); if (!mWidgetAdded) { mWidgetManager.addWidget(TopBarWidget.this); @@ -188,6 +202,14 @@ public void releaseWidget() { } }; + private Observer mIsTabsBarVisible = isTabsBarVisible -> { + adjustWindowPlacement(isTabsBarVisible.get() && mViewModel.getUsesHorizontalTabsBar().getValue().get()); + }; + + private Observer mUsesHorizontalTabsBar = usesHorizontalTabsBar -> { + adjustWindowPlacement(mViewModel.getIsTabsBarVisible().getValue().get() && usesHorizontalTabsBar.get()); + }; + public void setDelegate(TopBarWidget.Delegate aDelegate) { mDelegate = aDelegate; } diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TrayWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TrayWidget.java index 8c60006175..b0cc89bc90 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TrayWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TrayWidget.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; @@ -27,10 +28,12 @@ import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; import androidx.databinding.ObservableBoolean; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; import com.igalia.wolvic.R; import com.igalia.wolvic.VRBrowserActivity; @@ -59,8 +62,11 @@ import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.Objects; -public class TrayWidget extends UIWidget implements WidgetManagerDelegate.UpdateListener, DownloadsManager.DownloadsListener, ConnectivityReceiver.Delegate { +public class TrayWidget extends UIWidget implements WidgetManagerDelegate.UpdateListener, + DownloadsManager.DownloadsListener, ConnectivityReceiver.Delegate, + SharedPreferences.OnSharedPreferenceChangeListener { private static final int ICON_ANIMATION_DURATION = 200; @@ -117,9 +123,11 @@ private void initialize(Context aContext) { (VRBrowserActivity)getContext(), ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) .get(TrayViewModel.class); - mTrayViewModel.getIsVisible().observe((VRBrowserActivity) getContext(), mIsVisibleObserver); + mTrayViewModel.getIsVisible().observe((VRBrowserActivity) getContext(), mIsVisibleObserver); mTrayViewModel.setHeadsetBatteryLevel(R.drawable.ic_icon_statusbar_indicator_10); + mTrayViewModel.setTabsButtonInTray(SettingsStore.getInstance(getContext()).getTabsLocation() == SettingsStore.TABS_LOCATION_TRAY); + updateUI(); mIsWindowAttached = false; @@ -137,6 +145,8 @@ private void initialize(Context aContext) { mConnectivityReceived = ((VRBrowserApplication)getContext().getApplicationContext()).getConnectivityReceiver(); mConnectivityReceived.addListener(this); + PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + mWifiSSID = getContext().getString(R.string.tray_wifi_no_connection); updateTime(); @@ -919,4 +929,13 @@ private String getFormattedDate() { SimpleDateFormat.FULL, LocaleUtils.getDisplayLanguage(getContext()).getLocale()); return format.format(new Date()); } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) { + if (Objects.equals(key, getContext().getString(R.string.settings_key_tabs_location))) { + int value = sharedPreferences.getInt(key, SettingsStore.TABS_LOCATION_TRAY); + mTrayViewModel.setTabsButtonInTray(value == SettingsStore.TABS_LOCATION_TRAY); + updateUI(); + } + } } diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/VerticalTabsBar.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/VerticalTabsBar.java new file mode 100644 index 0000000000..ed741210c3 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/VerticalTabsBar.java @@ -0,0 +1,86 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.igalia.wolvic.R; +import com.igalia.wolvic.browser.SettingsStore; +import com.igalia.wolvic.browser.engine.SessionStore; +import com.igalia.wolvic.ui.adapters.TabsBarAdapter; + +public class VerticalTabsBar extends AbstractTabsBar { + + protected Button mAddTabButton; + protected RecyclerView mTabsList; + protected LinearLayoutManager mLayoutManager; + protected TabsBarAdapter mAdapter; + protected final TabDelegate mTabDelegate; + + public VerticalTabsBar(Context aContext, TabDelegate aDelegate) { + super(aContext); + mTabDelegate = aDelegate; + updateUI(); + } + + private void updateUI() { + removeAllViews(); + + inflate(getContext(), R.layout.tabs_bar_vertical, this); + + mAddTabButton = findViewById(R.id.add_tab); + mAddTabButton.setOnClickListener(v -> mTabDelegate.onTabAdd()); + + mTabsList = findViewById(R.id.tabsRecyclerView); + mLayoutManager = new LinearLayoutManager(getContext()); + mLayoutManager.setOrientation(RecyclerView.VERTICAL); + mTabsList.setLayoutManager(mLayoutManager); + mAdapter = new TabsBarAdapter(mTabDelegate, TabsBarAdapter.Orientation.VERTICAL); + mTabsList.setAdapter(mAdapter); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + Context context = getContext(); + aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.vertical_tabs_bar_width); + aPlacement.height = SettingsStore.getInstance(getContext()).getWindowHeight(); + aPlacement.worldWidth = aPlacement.width * WidgetPlacement.worldToDpRatio(context); + aPlacement.translationY = WidgetPlacement.dpDimension(context, R.dimen.top_bar_window_margin); + aPlacement.anchorX = 1.0f; + aPlacement.anchorY = 0.0f; + aPlacement.parentAnchorX = 0.0f; + aPlacement.parentAnchorY = 0.0f; + aPlacement.parentAnchorGravity = WidgetPlacement.GRAVITY_CENTER_Y; + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + @Override + public void updateWidgetPlacement() { + if (mAttachedWindow == null) { + mWidgetPlacement.parentHandle = -1; + } else { + mWidgetPlacement.parentHandle = mAttachedWindow.getHandle(); + mWidgetPlacement.height = mAttachedWindow.getPlacement().height; + } + } + + public void refreshTabs() { + mAdapter.updateTabs(SessionStore.get().getSessions(mPrivateMode)); + } + + @Override + public void attachToWindow(@NonNull WindowWidget window) { + super.attachToWindow(window); + } + + @Override + public void detachFromWindow() { + if (mAttachedWindow != null) { + mAttachedWindow.updatePlacementOffsetIfNeeded(); + } + super.detachFromWindow(); + } +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WidgetPlacement.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WidgetPlacement.java index 4f5101190b..119e07e523 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WidgetPlacement.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WidgetPlacement.java @@ -16,7 +16,6 @@ import com.igalia.wolvic.R; import com.igalia.wolvic.browser.SettingsStore; -import com.igalia.wolvic.utils.DeviceType; public class WidgetPlacement { static final float WORLD_DPI_RATIO = 2.0f/720.0f; @@ -83,6 +82,10 @@ public WidgetPlacement(Context aContext) { */ public float cylinderMapRadius; + // Translation offset (in world dimensions), applied after anchors. + public float horizontalOffset; + public float verticalOffset; + public WidgetPlacement clone() { WidgetPlacement w = new WidgetPlacement(); w.copyFrom(this); @@ -121,6 +124,8 @@ public void copyFrom(WidgetPlacement w) { this.name = w.name; this.clearColor = w.clearColor; this.cylinderMapRadius = w.cylinderMapRadius; + this.horizontalOffset = w.horizontalOffset; + this.verticalOffset = w.verticalOffset; } public void updateCylinderMapRadius() { diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java index 821e68f731..8cac68f594 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java @@ -170,6 +170,10 @@ public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferenc if (mViewModel.getIsDrmUsed().getValue().get() && getSession() != null) { getSession().reload(WSession.LOAD_FLAGS_BYPASS_CACHE); } + } else if (key.equals(getContext().getString(R.string.settings_key_tabs_location))) { + int tabsLocation = sharedPreferences.getInt(key, SettingsStore.TABS_LOCATION_TRAY); + mViewModel.setUsesHorizontalTabsBar(tabsLocation == SettingsStore.TABS_LOCATION_HORIZONTAL); + mViewModel.setUsesVerticalTabsBar(tabsLocation == SettingsStore.TABS_LOCATION_VERTICAL); } } @@ -207,6 +211,9 @@ private void initialize(Context aContext) { mViewModel.setIsPrivateSession(mSession.isPrivateMode()); mViewModel.setUrl(mSession.getCurrentUri()); mViewModel.setIsDesktopMode(mSession.getUaMode() == WSessionSettings.USER_AGENT_MODE_DESKTOP); + int tabsLocation = SettingsStore.getInstance(getContext()).getTabsLocation(); + mViewModel.setUsesHorizontalTabsBar(tabsLocation == SettingsStore.TABS_LOCATION_HORIZONTAL); + mViewModel.setUsesVerticalTabsBar(tabsLocation == SettingsStore.TABS_LOCATION_VERTICAL); // re-center the front window when its height changes mViewModel.getHeight().observe((VRBrowserActivity) getContext(), observableInt -> centerFrontWindowIfNeeded()); @@ -1055,6 +1062,12 @@ public void centerFrontWindowIfNeeded() { mWidgetManager.updateVisibleWidgets(); } + public void setOffset(float horizontalOffset, float verticalOffset) { + mWidgetPlacement.horizontalOffset = horizontalOffset; + mWidgetPlacement.verticalOffset = verticalOffset; + mWidgetManager.updateWidget(this); + } + @Override public void releaseWidget() { cleanListeners(mSession); diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/Windows.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/Windows.java index 7c65403cc5..dab125f92b 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/Windows.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/Windows.java @@ -65,7 +65,7 @@ import mozilla.components.concept.sync.TabData; public class Windows implements TrayListener, TopBarWidget.Delegate, TitleBarWidget.Delegate, - WindowWidget.WindowListener, TabsWidget.TabDelegate, Services.TabReceivedDelegate { + WindowWidget.WindowListener, TabDelegate, Services.TabReceivedDelegate { private static final String LOGTAG = SystemUtils.createLogtag(Windows.class); @@ -511,6 +511,7 @@ public void focusWindow(@Nullable WindowWidget aWindow) { if (mDelegate != null) { mDelegate.onFocusedWindowChanged(mFocusedWindow, prev); } + adjustWindowOffsets(); } } @@ -935,6 +936,7 @@ private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPositio placement.parentAnchorX = 0.0f; placement.parentAnchorY = 0.0f; placement.parentAnchorGravity = centerWindow ? WidgetPlacement.GRAVITY_CENTER_Y : WidgetPlacement.GRAVITY_DEFAULT; + placement.horizontalOffset = - WidgetPlacement.dpDimension(mContext, R.dimen.vertical_tabs_bar_width) * WidgetPlacement.worldToDpRatio(mContext); placement.rotationAxisX = 0; placement.rotationAxisZ = 0; if (curvedMode) { @@ -954,6 +956,7 @@ private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPositio placement.parentAnchorX = 1.0f; placement.parentAnchorY = 0.0f; placement.parentAnchorGravity = centerWindow ? WidgetPlacement.GRAVITY_CENTER_Y : WidgetPlacement.GRAVITY_DEFAULT; + placement.horizontalOffset = WidgetPlacement.dpDimension(mContext, R.dimen.vertical_tabs_bar_width) * WidgetPlacement.worldToDpRatio(mContext); placement.rotationAxisX = 0; placement.rotationAxisZ = 0; if (curvedMode) { @@ -970,6 +973,27 @@ private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPositio } } + public void adjustWindowOffsets() { + boolean verticalTabsBar = SettingsStore.getInstance(mContext).getTabsLocation() == SettingsStore.TABS_LOCATION_VERTICAL; + float tabsBarWidth = WidgetPlacement.dpDimension(mContext, R.dimen.vertical_tabs_bar_width) * WidgetPlacement.worldToDpRatio(mContext); + + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + + if (frontWindow != null) { + frontWindow.setOffset(0.0f, 0.0f); + } + + if (leftWindow != null) { + leftWindow.setOffset(((verticalTabsBar && frontWindow == mFocusedWindow)) ? -tabsBarWidth : 0.0f, 0.0f); + } + + if (rightWindow != null) { + rightWindow.setOffset(((verticalTabsBar && rightWindow == mFocusedWindow)) ? tabsBarWidth : 0.0f, 0.0f); + } + } + public void updateCurvedMode(boolean force) { float density = SettingsStore.getInstance(mContext).getCylinderDensity(); boolean storedCurvedMode = density > 0.0f; @@ -1425,6 +1449,7 @@ public void selectTab(@NonNull Session aTab) { public void onTabSelect(Session aTab) { if (mFocusedWindow.getSession() != aTab) { TelemetryService.Tabs.activatedEvent(); + aTab.updateLastUse(); } WindowWidget targetWindow = mFocusedWindow; diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/ContextMenuWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/ContextMenuWidget.java index 4d875f1032..14feee618b 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/ContextMenuWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/ContextMenuWidget.java @@ -101,7 +101,7 @@ public void setContextElement(WSession.ContentDelegate.ContextElement aContextEl // Open link in a new tab mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_open_link_new_tab_1), 0, () -> { if (!StringUtils.isEmpty(aContextElement.linkUri)) { - widgetManager.openNewTab(aContextElement.linkUri); + widgetManager.openNewTabForeground(aContextElement.linkUri); TelemetryService.Tabs.openedCounter(TelemetryService.Tabs.TabSource.CONTEXT_MENU); } onDismiss(); diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/settings/DisplayOptionsView.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/settings/DisplayOptionsView.java index abf9348e24..1ad051593b 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/settings/DisplayOptionsView.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/settings/DisplayOptionsView.java @@ -107,6 +107,10 @@ protected void updateUI() { mBinding.headLockSwitch.setOnCheckedChangeListener(mHeadLockListener); setHeadLock(SettingsStore.getInstance(getContext()).isHeadLockEnabled(), false); + @SettingsStore.TabsLocation int tabsLocation = SettingsStore.getInstance(getContext()).getTabsLocation(); + mBinding.tabsLocationRadio.setOnCheckedChangeListener(mTabsLocationChangeListener); + setTabsLocation(mBinding.tabsLocationRadio.getIdForValue(tabsLocation), false); + mDefaultHomepageUrl = getContext().getString(R.string.HOMEPAGE_URL); mBinding.homepageEdit.setHint1(getContext().getString(R.string.homepage_hint, getContext().getString(R.string.app_name))); @@ -200,6 +204,10 @@ public boolean isEditing() { setHeadLock(value, true); }; + private RadioGroupSetting.OnCheckedChangeListener mTabsLocationChangeListener = (radioGroup, checkedId, doApply) -> { + setTabsLocation(checkedId, true); + }; + private OnClickListener mHomepageListener = (view) -> { if (!mBinding.homepageEdit.getFirstText().isEmpty()) { setHomepage(mBinding.homepageEdit.getFirstText()); @@ -259,6 +267,9 @@ public boolean isEditing() { setMSAAMode(mBinding.msaaRadio.getIdForValue(SettingsStore.MSAA_DEFAULT_LEVEL), true); restart = true; } + if (!mBinding.tabsLocationRadio.getValueForId(mBinding.tabsLocationRadio.getCheckedRadioButtonId()).equals(SettingsStore.TABS_LOCATION_DEFAULT)) { + setTabsLocation(mBinding.tabsLocationRadio.getIdForValue(SettingsStore.TABS_LOCATION_DEFAULT), true); + } if (mBinding.windowsSize.getCheckedRadioButtonId() != SettingsStore.WINDOW_SIZE_PRESET_DEFAULT.ordinal()) { setWindowsSizePreset(SettingsStore.WINDOW_SIZE_PRESET_DEFAULT.ordinal(), true); @@ -365,6 +376,17 @@ private void setSoundEffect(boolean value, boolean doApply) { } } + private void setTabsLocation(int checkedId, boolean doApply) { + mBinding.tabsLocationRadio.setOnCheckedChangeListener(null); + mBinding.tabsLocationRadio.setChecked(checkedId, doApply); + mBinding.tabsLocationRadio.setOnCheckedChangeListener(mTabsLocationChangeListener); + + if (doApply) { + int tabsLocationValue = (Integer) mBinding.tabsLocationRadio.getValueForId(checkedId); + SettingsStore.getInstance(getContext()).setTabsLocation(tabsLocationValue); + } + } + private void setHomepage(String newHomepage) { mBinding.homepageEdit.setOnClickListener(null); mBinding.homepageEdit.setFirstText(newHomepage); diff --git a/app/src/main/cpp/BrowserWorld.cpp b/app/src/main/cpp/BrowserWorld.cpp index 8bdaf35e2d..17453b8d9d 100644 --- a/app/src/main/cpp/BrowserWorld.cpp +++ b/app/src/main/cpp/BrowserWorld.cpp @@ -1607,12 +1607,16 @@ BrowserWorld::LayoutWidget(int32_t aHandle) { } else { parentTranslationX = parentWorldWith * aPlacement->parentAnchor.x() - parentWorldWith * 0.5f; } + parentTranslationX += (aPlacement->horizontalOffset); + if (aPlacement->parentAnchorGravity & WidgetPlacement::kParentAnchorGravityCenterY) { parentTranslationY = (parentWorldHeight - worldHeight) / 2.0f - parentWorldHeight * 0.5f; } else { parentTranslationY = parentWorldHeight * aPlacement->parentAnchor.y() - parentWorldHeight * 0.5f; } + parentTranslationY += (aPlacement->verticalOffset); + translation += vrb::Vector(parentTranslationX, parentTranslationY, 0.0f); } diff --git a/app/src/main/cpp/WidgetPlacement.cpp b/app/src/main/cpp/WidgetPlacement.cpp index eaae072296..c618f5486a 100644 --- a/app/src/main/cpp/WidgetPlacement.cpp +++ b/app/src/main/cpp/WidgetPlacement.cpp @@ -76,6 +76,8 @@ WidgetPlacement::FromJava(JNIEnv* aEnv, jobject& aObject) { GET_INT_FIELD(borderColor); GET_STRING_FIELD(name); GET_INT_FIELD(clearColor); + GET_FLOAT_FIELD(horizontalOffset, "horizontalOffset"); + GET_FLOAT_FIELD(verticalOffset, "verticalOffset"); return result; } diff --git a/app/src/main/cpp/WidgetPlacement.h b/app/src/main/cpp/WidgetPlacement.h index 9e0e0c113e..542fc1f87a 100644 --- a/app/src/main/cpp/WidgetPlacement.h +++ b/app/src/main/cpp/WidgetPlacement.h @@ -48,6 +48,8 @@ struct WidgetPlacement { int borderColor; std::string name; int clearColor; + float horizontalOffset; + float verticalOffset; int32_t GetTextureWidth() const; int32_t GetTextureHeight() const; diff --git a/app/src/main/res/drawable/tabs_bar_bg.xml b/app/src/main/res/drawable/tabs_bar_bg.xml new file mode 100644 index 0000000000..7172380903 --- /dev/null +++ b/app/src/main/res/drawable/tabs_bar_bg.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tabs_bar_item_bg.xml b/app/src/main/res/drawable/tabs_bar_item_bg.xml new file mode 100644 index 0000000000..3c400a2a03 --- /dev/null +++ b/app/src/main/res/drawable/tabs_bar_item_bg.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/options_display.xml b/app/src/main/res/layout/options_display.xml index 137ea71ce2..8c76ada5ad 100644 --- a/app/src/main/res/layout/options_display.xml +++ b/app/src/main/res/layout/options_display.xml @@ -149,6 +149,14 @@ android:layout_height="wrap_content" app:description="@string/display_options_head_lock" /> + + diff --git a/app/src/main/res/layout/tabs_bar_horizontal.xml b/app/src/main/res/layout/tabs_bar_horizontal.xml new file mode 100644 index 0000000000..4427e1c949 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_horizontal.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tabs_bar_item_horizontal.xml b/app/src/main/res/layout/tabs_bar_item_horizontal.xml new file mode 100644 index 0000000000..3e29bf3139 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_item_horizontal.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tabs_bar_item_vertical.xml b/app/src/main/res/layout/tabs_bar_item_vertical.xml new file mode 100644 index 0000000000..35e5cdc643 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_item_vertical.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tabs_bar_vertical.xml b/app/src/main/res/layout/tabs_bar_vertical.xml new file mode 100644 index 0000000000..9352f21138 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_vertical.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tray.xml b/app/src/main/res/layout/tray.xml index a026fb8c10..b58558a9c1 100644 --- a/app/src/main/res/layout/tray.xml +++ b/app/src/main/res/layout/tray.xml @@ -169,6 +169,7 @@ app:tooltipDensity="@dimen/tray_tooltip_density" app:tooltipPosition="bottom" app:tooltipLayout="@layout/tooltip_tray" + visibleGone="@{traymodel.tabsButtonInTray}" android:src="@drawable/ic_icon_tray_tabs" app:regularModeBackground="@{traymodel.isMaxWindows ? @drawable/tray_background_unchecked_start : @drawable/tray_background_unchecked_middle}" app:privateModeBackground="@{traymodel.isMaxWindows ? @drawable/tray_background_start_private : @drawable/tray_background_middle_private}" @@ -182,7 +183,10 @@ app:tooltipPosition="bottom" app:tooltipLayout="@layout/tooltip_tray" android:src="@{viewmodel.isPrivateSession ? @drawable/ic_icon_tray_private_browsing_on_v2 : @drawable/ic_icon_tray_private_browsing_v2}" - app:privateMode="@{viewmodel.isPrivateSession}"/> + app:privateMode="@{viewmodel.isPrivateSession}" + app:regularModeBackground="@{traymodel.isMaxWindows && viewmodel.isTabsBarVisible ? @drawable/tray_background_unchecked_start : @drawable/tray_background_unchecked_middle}" + app:privateModeBackground="@{traymodel.isMaxWindows && viewmodel.isTabsBarVisible ? @drawable/tray_background_start_private : @drawable/tray_background_middle_private}" + app:activeModeBackground="@{traymodel.isMaxWindows && viewmodel.isTabsBarVisible ? @drawable/tray_background_checked_start : @drawable/tray_background_checked_middle}"/> 7dp 3dp + + 200dp + 48dp + 52dp + 128dp + 400dp 200dp diff --git a/app/src/main/res/values/non_L10n.xml b/app/src/main/res/values/non_L10n.xml index 31bcd227e3..241f7e1b72 100644 --- a/app/src/main/res/values/non_L10n.xml +++ b/app/src/main/res/values/non_L10n.xml @@ -85,6 +85,7 @@ settings_key_privacy_policy_accepted settings_key_search_engine_id settings_key_eye_tracking_supported + settings_key_tabs_location https://github.com/igalia/wolvic/wiki/Environments https://wolvic.com/legal/privacy/ https://www.igalia.com/privacy/&url=%1$s diff --git a/app/src/main/res/values/options_values.xml b/app/src/main/res/values/options_values.xml index 9619e59488..6f7d89cbe2 100644 --- a/app/src/main/res/values/options_values.xml +++ b/app/src/main/res/values/options_values.xml @@ -85,6 +85,23 @@ 2 + + + @string/display_options_tabs_location_tray + @string/display_options_tabs_location_horizontal + @string/display_options_tabs_location_vertical + + + + + 0 + + 1 + + 2 + + + @string/privacy_options_tracking_etp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db747ad453..d7c812a2f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -499,6 +499,14 @@ and is used to customize if we use passthrough mode when the app starts. --> Start with Passthrough Mode + + Tabs location + + Tray + Horizontal + Vertical + Use Sound Effects