diff --git a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java index a7ac337d48..14db647be6 100644 --- a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java +++ b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java @@ -69,13 +69,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; @@ -105,6 +108,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; @@ -213,6 +217,7 @@ public void run() { RootWidget mRootWidget; KeyboardWidget mKeyboard; NavigationBarWidget mNavigationBar; + AbstractTabsBar mTabsBar; CrashDialogWidget mCrashDialog; TrayWidget mTray; WhatsNewWidget mWhatsNewWidget = null; @@ -465,9 +470,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); @@ -483,10 +498,18 @@ private void attachToWindow(@NonNull WindowWidget aWindow, @Nullable WindowWidge mKeyboard.attachToWindow(aWindow); mTray.attachToWindow(aWindow); + if (mTabsBar != null) { + mTabsBar.attachToWindow(aWindow); + } + if (aPrevWindow != null) { updateWidget(mNavigationBar); + updateWidget(mTabsBar); updateWidget(mKeyboard); updateWidget(mTray); + if (mTabsBar != null) { + updateWidget(mTabsBar); + } } } @@ -772,14 +795,40 @@ 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))) { + + Log.e(LOGTAG, "update tabs location"); + + // remove the previous widget + if (mTabsBar != null) { + Log.e(LOGTAG, "remove previous widget"); + removeWidget(mTabsBar); + } + + if (mSettings.getTabsLocation() == SettingsStore.TABS_LOCATION_TRAY) { + mTabsBar = null; + } else { + 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 { + Log.e(LOGTAG, "Invalid value for tabs location"); + mTabsBar = null; + return; + } + addWidget(mTabsBar); + mTabsBar.attachToWindow(mWindows.getFocusedWindow()); + updateWidget(mTabsBar); } + } } 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 cb609ba28d..b84a9faf69 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; @@ -113,6 +119,7 @@ SettingsStore getInstance(final @NonNull Context aContext) { 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; @@ -374,6 +381,18 @@ public void setWindowMovementEnabled(boolean isEnabled) { mSettingsViewModel.setWindowMovementEnabled(isEnabled); } + @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/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..27d165d997 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/adapters/TabsBarAdapter.java @@ -0,0 +1,94 @@ +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) { + if (position == 0) { + return 0; + } else { + return 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..5ddbea94bd 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 @@ -11,6 +11,7 @@ import androidx.lifecycle.Observer; import com.igalia.wolvic.R; +import com.igalia.wolvic.browser.SettingsStore; public class TrayViewModel extends AndroidViewModel { @@ -21,6 +22,7 @@ public class TrayViewModel extends AndroidViewModel { private MediatorLiveData isVisible; private MutableLiveData time; private MutableLiveData pm; + private MutableLiveData needsTabsButton; private MutableLiveData wifiConnected; private MutableLiveData headsetIcon; private MutableLiveData headsetBatteryLevel; @@ -43,7 +45,7 @@ public TrayViewModel(@NonNull Application application) { isVisible.setValue(new ObservableBoolean(false)); time = new MutableLiveData<>(); pm = new MutableLiveData<>(); - pm = new MutableLiveData<>(); + needsTabsButton = 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 +71,7 @@ public void refresh() { isKeyboardVisible.setValue(isKeyboardVisible.getValue()); time.postValue(time.getValue()); pm.postValue(pm.getValue()); + needsTabsButton.postValue(needsTabsButton.getValue()); wifiConnected.postValue(wifiConnected.getValue()); headsetIcon.setValue(headsetIcon.getValue()); headsetBatteryLevel.setValue(headsetBatteryLevel.getValue()); @@ -127,6 +130,14 @@ public MutableLiveData getPm() { return pm; } + public void setNeedsTabsButton(boolean needsTabs) { + this.needsTabsButton.setValue(new ObservableBoolean(needsTabs)); + } + + public MutableLiveData getNeedsTabsButton() { + return needsTabsButton; + } + public void setWifiConnected(boolean connected) { this.wifiConnected.setValue(new ObservableBoolean(connected)); } diff --git a/app/src/common/shared/com/igalia/wolvic/ui/views/CustomFastScroller.java b/app/src/common/shared/com/igalia/wolvic/ui/views/CustomFastScroller.java index f4682ccf40..a3dd1cbd9e 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/views/CustomFastScroller.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/views/CustomFastScroller.java @@ -361,7 +361,7 @@ void updateScrollPosition(int offsetX, int offsetY) { int verticalContentLength = mRecyclerView.computeVerticalScrollRange(); int verticalVisibleLength = mRecyclerViewHeight; mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0 - && mRecyclerViewHeight >= mScrollbarMinimumRange || mNeedHorizontalScrollbar; + && mRecyclerViewHeight >= mScrollbarMinimumRange || mNeedVerticalScrollbar; int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange(); int horizontalVisibleLength = mRecyclerViewWidth; 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..e61f0d04bf --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java @@ -0,0 +1,163 @@ +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.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.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 UIButton 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); + + 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..7d49aeaced --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/AbstractTabsBar.java @@ -0,0 +1,66 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +import com.igalia.wolvic.browser.SessionChangeListener; +import com.igalia.wolvic.browser.api.WSession; +import com.igalia.wolvic.browser.engine.Session; + +public abstract class AbstractTabsBar extends UIWidget implements SessionChangeListener { + + public AbstractTabsBar(Context aContext) { + super(aContext); + } + + public AbstractTabsBar(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + } + + public AbstractTabsBar(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + } + + // TODO Use more fine-grained updates. + public abstract void refreshTabs(); + + @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..4077ffddb4 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/HorizontalTabsBar.java @@ -0,0 +1,81 @@ +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.SessionChangeListener; +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 implements SessionChangeListener { + + protected boolean mPrivateMode; + 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); + + SessionStore.get().addSessionChangeListener(this); + } + + @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 = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + @Override + public void attachToWindow(@NonNull WindowWidget window) { + super.attachToWindow(window); + mPrivateMode = window.getSession().isPrivateMode(); + mWidgetPlacement.parentHandle = window.getHandle(); + mWidgetPlacement.width = window.getPlacement().width; + refreshTabs(); + } + + @Override + public void detachFromWindow() { + super.detachFromWindow(); + } + + 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..b797a9d062 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java @@ -0,0 +1,11 @@ +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); +} 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 24f8a19fe8..33fced85d8 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 @@ -47,12 +47,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); - } - public TabsWidget(Context aContext) { super(aContext); mBitmapCache = BitmapCache.getInstance(aContext); 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 a8c6773d1a..f8936530de 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 @@ -11,6 +11,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; @@ -30,12 +31,13 @@ import android.view.animation.AccelerateDecelerateInterpolator; 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.BuildConfig; import com.igalia.wolvic.R; import com.igalia.wolvic.VRBrowserActivity; import com.igalia.wolvic.VRBrowserApplication; @@ -63,8 +65,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; @@ -121,9 +126,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.setNeedsTabsButton(SettingsStore.getInstance(getContext()).getTabsLocation() == SettingsStore.TABS_LOCATION_TRAY); + updateUI(); mIsWindowAttached = false; @@ -141,6 +148,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(); @@ -924,4 +933,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.setNeedsTabsButton(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..1c6e130fd0 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/VerticalTabsBar.java @@ -0,0 +1,82 @@ +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.SessionChangeListener; +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 implements SessionChangeListener { + + protected boolean mPrivateMode; + protected Button mAddTabButton; + protected RecyclerView mTabsList; + protected LinearLayoutManager mLayoutManager; + protected TabsBarAdapter mAdapter; + protected 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); + + SessionStore.get().addSessionChangeListener(this); + } + + @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.layer = true; + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + @Override + public void attachToWindow(@NonNull WindowWidget window) { + super.attachToWindow(window); + mPrivateMode = window.getSession().isPrivateMode(); + mWidgetPlacement.parentHandle = window.getHandle(); + mWidgetPlacement.height = window.getPlacement().height; + refreshTabs(); + } + + @Override + public void detachFromWindow() { + super.detachFromWindow(); + } + + public void refreshTabs() { + mAdapter.updateTabs(SessionStore.get().getSessions(mPrivateMode)); + } +} 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 f48ee4df19..981066bd78 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 @@ -62,7 +62,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); @@ -1387,6 +1387,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 68e8f63af3..553a79fced 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 @@ -96,6 +96,10 @@ protected void updateUI() { mBinding.windowMovementSwitch.setOnCheckedChangeListener(mWindowMovementListener); setWindowMovement(SettingsStore.getInstance(getContext()).isWindowMovementEnabled(), 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))); @@ -189,6 +193,10 @@ public boolean isEditing() { setWindowMovement(enabled, true); }; + private RadioGroupSetting.OnCheckedChangeListener mTabsLocationChangeListener = (radioGroup, checkedId, doApply) -> { + setTabsLocation(checkedId, true); + }; + private OnClickListener mHomepageListener = (view) -> { if (!mBinding.homepageEdit.getFirstText().isEmpty()) { setHomepage(mBinding.homepageEdit.getFirstText()); @@ -241,6 +249,9 @@ public boolean isEditing() { if (!mBinding.msaaRadio.getValueForId(mBinding.msaaRadio.getCheckedRadioButtonId()).equals(SettingsStore.MSAA_DEFAULT_LEVEL)) { setMSAAMode(mBinding.msaaRadio.getIdForValue(SettingsStore.MSAA_DEFAULT_LEVEL), true); } + if (!mBinding.tabsLocationRadio.getValueForId(mBinding.tabsLocationRadio.getCheckedRadioButtonId()).equals(SettingsStore.TABS_LOCATION_DEFAULT)) { + setTabsLocation(mBinding.tabsLocationRadio.getIdForValue(SettingsStore.TABS_LOCATION_DEFAULT), true); + } restart = restart | setDisplayDensity(SettingsStore.DISPLAY_DENSITY_DEFAULT); restart = restart | setDisplayDpi(SettingsStore.DISPLAY_DPI_DEFAULT); @@ -361,6 +372,17 @@ private void setWindowMovement(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/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 d6e8de6dbd..cf15677c9c 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_windows_can_move" /> + + 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..5b66959242 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_horizontal.xml @@ -0,0 +1,37 @@ + + + + + + + + \ 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..950116b877 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_item_horizontal.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + \ 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..b9d408fd59 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_item_vertical.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + \ 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..713aae2267 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_vertical.xml @@ -0,0 +1,37 @@ + + + + + + + + \ 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 21ac2d6c6b..607c27ec30 100644 --- a/app/src/main/res/layout/tray.xml +++ b/app/src/main/res/layout/tray.xml @@ -168,6 +168,7 @@ app:tooltipDensity="@dimen/tray_tooltip_density" app:tooltipPosition="bottom" app:tooltipLayout="@layout/tooltip_tray" + visibleGone="@{traymodel.needsTabsButton}" 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}" diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 0282b7be26..3322bb6fdf 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -115,6 +115,12 @@ 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 583beb6c3f..0c22302266 100644 --- a/app/src/main/res/values/non_L10n.xml +++ b/app/src/main/res/values/non_L10n.xml @@ -83,6 +83,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 0a9a25fecc..daf28ea226 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -503,6 +503,14 @@ customize if windows can be moved. Enabling it adds a Move button to the navigation bar. --> Drag to move windows + + Tabs location + + Tray + Horizontal + Vertical + Use Sound Effects