From 5a24a578e78f44e1b2b138fe8ea050ba8bbbdddb Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Tue, 1 Oct 2024 14:35:23 +0900 Subject: [PATCH] Initial implementation of browser tabs The Tabs Bar is a small window that sits next to the current window and which lists the open tabs. There are two implementations: - HorizontalTabsBar on the top of the window - VerticalTabsBar on the side of the window The Tabs Bar is managed by VRBrowserActivity. SessionStore now keeps a list of session change listeners. TabDelegate is moved to a separate interface. The current tabs style can be changed in Settings -> Display. WindowViewModel gets three new fields: - isTabsBarVisible - usesHorizontalTabsBar - usesVerticalTabsBar TopBarWidget uses two of these fields to update its placement so as to not overlap with the tabs bar. Note that we don't (yet) link windows and tabs, so the interface gets a bit confusing when we have more than one window open. WidgetPlacement gets two new fields: horizontalOffset, verticalOffset. These define a translation offset (in world dimensions), which is applied after the anchors have been calculated. The goal is to be able to move widgets from their initial position in a predictable way, to make space for the tabs bar when necessary. --- .../com/igalia/wolvic/VRBrowserActivity.java | 62 ++++++- .../igalia/wolvic/browser/SettingsStore.java | 19 ++ .../igalia/wolvic/browser/engine/Session.java | 3 + .../wolvic/browser/engine/SessionStore.java | 33 ++++ .../wolvic/ui/adapters/TabsBarAdapter.java | 90 +++++++++ .../wolvic/ui/viewmodel/TrayViewModel.java | 12 +- .../wolvic/ui/viewmodel/WindowViewModel.java | 49 +++++ .../igalia/wolvic/ui/views/TabsBarItem.java | 174 ++++++++++++++++++ .../wolvic/ui/widgets/AbstractTabsBar.java | 146 +++++++++++++++ .../wolvic/ui/widgets/HorizontalTabsBar.java | 73 ++++++++ .../igalia/wolvic/ui/widgets/TabDelegate.java | 12 ++ .../igalia/wolvic/ui/widgets/TabsWidget.java | 7 - .../wolvic/ui/widgets/TopBarWidget.java | 26 ++- .../igalia/wolvic/ui/widgets/TrayWidget.java | 23 ++- .../wolvic/ui/widgets/VerticalTabsBar.java | 86 +++++++++ .../wolvic/ui/widgets/WidgetPlacement.java | 7 +- .../wolvic/ui/widgets/WindowWidget.java | 13 ++ .../com/igalia/wolvic/ui/widgets/Windows.java | 27 ++- .../ui/widgets/menus/ContextMenuWidget.java | 2 +- .../widgets/settings/DisplayOptionsView.java | 22 +++ app/src/main/cpp/BrowserWorld.cpp | 4 + app/src/main/cpp/WidgetPlacement.cpp | 2 + app/src/main/cpp/WidgetPlacement.h | 2 + app/src/main/res/drawable/tabs_bar_bg.xml | 13 ++ .../main/res/drawable/tabs_bar_item_bg.xml | 39 ++++ app/src/main/res/layout/options_display.xml | 8 + .../main/res/layout/tabs_bar_horizontal.xml | 38 ++++ .../res/layout/tabs_bar_item_horizontal.xml | 74 ++++++++ .../res/layout/tabs_bar_item_vertical.xml | 80 ++++++++ app/src/main/res/layout/tabs_bar_vertical.xml | 38 ++++ app/src/main/res/layout/tray.xml | 6 +- app/src/main/res/values/dimen.xml | 6 + app/src/main/res/values/non_L10n.xml | 1 + app/src/main/res/values/options_values.xml | 17 ++ app/src/main/res/values/strings.xml | 8 + 35 files changed, 1198 insertions(+), 24 deletions(-) create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/adapters/TabsBarAdapter.java create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/widgets/AbstractTabsBar.java create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/widgets/HorizontalTabsBar.java create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/widgets/VerticalTabsBar.java create mode 100644 app/src/main/res/drawable/tabs_bar_bg.xml create mode 100644 app/src/main/res/drawable/tabs_bar_item_bg.xml create mode 100644 app/src/main/res/layout/tabs_bar_horizontal.xml create mode 100644 app/src/main/res/layout/tabs_bar_item_horizontal.xml create mode 100644 app/src/main/res/layout/tabs_bar_item_vertical.xml create mode 100644 app/src/main/res/layout/tabs_bar_vertical.xml diff --git a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java index 50ef7a14a45..6485fcd9757 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 f6ac43ea7d4..d239032fd1c 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 37a2f269d10..8dc0db430a4 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 282dc72a59c..6010e799d2d 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 00000000000..97dfa990b62 --- /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 12b09340382..7f62a689b41 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 c44a285ed4a..b171b3f6056 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 00000000000..a38022d5d7e --- /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 00000000000..4b338b74da2 --- /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 00000000000..48b4c3c83ab --- /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 00000000000..efc6f41834f --- /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 b0c44d92190..f8e45430d20 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 48756e03ede..b53fbdb8a33 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 8c600061750..b0cc89bc903 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 00000000000..ed741210c38 --- /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 4f5101190b1..119e07e5237 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 821e68f7313..8cac68f5947 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 7c65403cc57..dab125f92b4 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 4d875f10320..14feee618b6 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 abf9348e248..1ad051593bf 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 8bdaf35e2db..17453b8d9d6 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 eaae0722960..c618f5486ab 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 9e0e0c113e9..542fc1f87a1 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 00000000000..71723809034 --- /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 00000000000..3c400a2a039 --- /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 137ea71ce24..8c76ada5adc 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 00000000000..4427e1c949e --- /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 00000000000..3e29bf31395 --- /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 00000000000..35e5cdc643f --- /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 00000000000..9352f21138a --- /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 a026fb8c10f..b58558a9c10 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 31bcd227e3e..241f7e1b720 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 9619e594885..6f7d89cbe23 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 db747ad4535..d7c812a2f1c 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