diff --git a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java index 85159cc84d..49dba6afcf 100644 --- a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java +++ b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java @@ -80,6 +80,7 @@ import com.igalia.wolvic.ui.widgets.Widget; import com.igalia.wolvic.ui.widgets.WidgetManagerDelegate; import com.igalia.wolvic.ui.widgets.WidgetPlacement; +import com.igalia.wolvic.ui.widgets.TabsBar; import com.igalia.wolvic.ui.widgets.WindowWidget; import com.igalia.wolvic.ui.widgets.Windows; import com.igalia.wolvic.ui.widgets.dialogs.CrashDialogWidget; @@ -213,6 +214,7 @@ public void run() { RootWidget mRootWidget; KeyboardWidget mKeyboard; NavigationBarWidget mNavigationBar; + TabsBar mTabsBar; CrashDialogWidget mCrashDialog; TrayWidget mTray; WhatsNewWidget mWhatsNewWidget = null; @@ -455,13 +457,17 @@ public void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { } }); + // Create Tabs bar widget + mTabsBar = new TabsBar(this); + mTabsBar.setTabDelegate(mWindows); + // Create the tray mTray = new TrayWidget(this); mTray.addListeners(mWindows); mTray.setAddWindowVisible(mWindows.canOpenNewWindow()); attachToWindow(mWindows.getFocusedWindow(), null); - addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mKeyboard, mTray, mWebXRInterstitial)); + addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mTabsBar, mKeyboard, mTray, mWebXRInterstitial)); // Create the platform plugin after widgets are created to be extra safe. mPlatformPlugin = createPlatformPlugin(this); @@ -474,11 +480,13 @@ public void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { private void attachToWindow(@NonNull WindowWidget aWindow, @Nullable WindowWidget aPrevWindow) { mPermissionDelegate.setParentWidgetHandle(aWindow.getHandle()); mNavigationBar.attachToWindow(aWindow); + mTabsBar.attachToWindow(aWindow); mKeyboard.attachToWindow(aWindow); mTray.attachToWindow(aWindow); if (aPrevWindow != null) { updateWidget(mNavigationBar); + updateWidget(mTabsBar); updateWidget(mKeyboard); updateWidget(mTray); } 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/views/TabsBarItem.java b/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java new file mode 100644 index 0000000000..359517ebf2 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/views/TabsBarItem.java @@ -0,0 +1,178 @@ +package com.igalia.wolvic.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +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.api.WSession; +import com.igalia.wolvic.browser.engine.Session; +import com.igalia.wolvic.browser.engine.SessionStore; +import com.igalia.wolvic.utils.SystemUtils; +import com.igalia.wolvic.utils.UrlUtils; + +import mozilla.components.browser.icons.IconRequest; + +public class TabsBarItem extends RelativeLayout implements WSession.ContentDelegate, WSession.NavigationDelegate { + + private static final String LOGTAG = SystemUtils.createLogtag(TabsBarItem.class); + + public enum Mode {ADD_TAB, TAB_DETAILS} + + protected Mode mMode = Mode.TAB_DETAILS; + protected ViewGroup mTabDetailsView; + protected ViewGroup mAddTabView; + 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 onAdd(TabsBarItem aSender); + + 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); + mAddTabView = findViewById(R.id.add_tab); + + 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); + + setMode(mMode); + } + + private final OnClickListener mClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (mDelegate != null) { + if (mMode == Mode.ADD_TAB) { + mDelegate.onAdd(TabsBarItem.this); + } else { + mDelegate.onClick(TabsBarItem.this); + } + } + } + }; + + public void attachToSession(@Nullable Session aSession) { + if (mSession != null) { + mSession.removeContentListener(this); + } + + mSession = aSession; + if (mSession != null) { + mSession.addContentListener(this); + + Log.e(LOGTAG, "attachToSession: " + mSession.getCurrentTitle() + " " + mSession.getCurrentUri()); + + 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; + } + + public void reset() { + mCloseButton.setHovered(false); + setMode(Mode.TAB_DETAILS); + } + + public void setMode(Mode mode) { + mMode = mode; + mAddTabView.setVisibility((mMode == Mode.ADD_TAB) ? VISIBLE : GONE); + mTabDetailsView.setVisibility((mMode == Mode.TAB_DETAILS) ? VISIBLE : GONE); + } + + @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 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/TabDelegate.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabDelegate.java new file mode 100644 index 0000000000..cd093de35a --- /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 onTabSelect(Session aTab); + void onTabAdd(); + void onTabsClose(List aTabs); +} diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsBar.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsBar.java new file mode 100644 index 0000000000..c14c4069eb --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/TabsBar.java @@ -0,0 +1,250 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +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.views.TabsBarItem; +import com.igalia.wolvic.utils.BitmapCache; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TabsBar extends UIWidget implements SessionChangeListener { + + protected BitmapCache mBitmapCache; + protected RecyclerView mTabsList; + protected GridLayoutManager mLayoutManager; + protected TabsBarAdapter mAdapter; + protected boolean mPrivateMode; + protected TabDelegate mTabDelegate; + + public TabsBar(Context aContext) { + super(aContext); + updateUI(); + } + + public TabsBar(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + updateUI(); + } + + public TabsBar(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + updateUI(); + } + + private void updateUI() { + if (mBitmapCache == null) { + mBitmapCache = BitmapCache.getInstance(getContext()); + } + + removeAllViews(); + inflate(getContext(), R.layout.tabs_bar, this); + mTabsList = findViewById(R.id.tabsRecyclerView); + mTabsList.setHasFixedSize(true); + mLayoutManager = new GridLayoutManager(getContext(), 1); + mTabsList.setLayoutManager(mLayoutManager); + mAdapter = new TabsBarAdapter(); + mTabsList.setAdapter(mAdapter); + + SessionStore.get().addSessionChangeListener(this); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + Context context = getContext(); + aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.tabs_bar_width); + aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.tabs_bar_height); + 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; + } + + @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 setTabDelegate(TabDelegate aDelegate) { + mTabDelegate = aDelegate; + } + + public void refreshTabs() { + mAdapter.updateTabs(SessionStore.get().getSessions(mPrivateMode)); + } + + @Override + public void updatePlacementTranslationZ() { + getPlacement().translationZ = WidgetPlacement.getWindowWorldZMeters(getContext()); + } + + @Override + public void onSessionAdded(Session aSession) { + // who calls this??? + Log.e(LOGTAG, "onSessionAdded: " + aSession); + refreshTabs(); + } + + @Override + public void onSessionOpened(Session aSession) { + Log.e(LOGTAG, "onSessionOpened: " + aSession); + refreshTabs(); + } + + @Override + public void onSessionClosed(Session aSession) { + Log.e(LOGTAG, "onSessionClosed: " + aSession); + refreshTabs(); + } + + @Override + public void onSessionRemoved(String aId) { + Log.e(LOGTAG, "onSessionRemoved: " + aId); + refreshTabs(); + } + + @Override + public void onSessionStateChanged(Session aSession, boolean aActive) { + Log.e(LOGTAG, "onSessionStateChanged: " + aSession + ", " + aActive); + refreshTabs(); + } + + @Override + public void onCurrentSessionChange(WSession aOldSession, WSession aSession) { + Log.e(LOGTAG, "onCurrentSessionChange: old " + aSession + ", new " + aSession); + refreshTabs(); + } + + @Override + public void onStackSession(Session aSession) { + Log.e(LOGTAG, "onStackSession: " + aSession); + refreshTabs(); + } + + @Override + public void onUnstackSession(Session aSession, Session aParent) { + Log.e(LOGTAG, "onUnstackSession: " + aSession); + refreshTabs(); + } + + public class TabsBarAdapter extends RecyclerView.Adapter { + private List mTabs = new ArrayList<>(); + + class ViewHolder extends RecyclerView.ViewHolder { + TabsBarItem mTabBarItem; + + ViewHolder(TabsBarItem v) { + super(v); + mTabBarItem = v; + } + } + + TabsBarAdapter() { + } + + @Override + public long getItemId(int position) { + if (position == 0) { + return 0; + } else { + return mTabs.get(position - 1).getId().hashCode(); + } + } + + void updateTabs(List aTabs) { + mTabs = aTabs; + + Log.e(LOGTAG, "updateTabs: " + aTabs.size()); + for (Session session : aTabs) { + Log.e(LOGTAG, " " + session.getCurrentUri() + " " + session.getLastUse()); + } + + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TabsBarItem view = (TabsBarItem) LayoutInflater.from(parent.getContext()).inflate(R.layout.tabs_bar_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (position > 0) { + Session session = mTabs.get(position - 1); + holder.mTabBarItem.attachToSession(session); + holder.mTabBarItem.setMode(TabsBarItem.Mode.TAB_DETAILS); + holder.mTabBarItem.setDelegate(mItemDelegate); + } else { + holder.mTabBarItem.attachToSession(null); + holder.mTabBarItem.setMode(TabsBarItem.Mode.ADD_TAB); + } + } + + @Override + public int getItemCount() { + return mTabs.size() + 1; + } + + private final TabsBarItem.Delegate mItemDelegate = new TabsBarItem.Delegate() { + @Override + public void onAdd(TabsBarItem item) { + + Log.e(LOGTAG, "TabsBarItem.Delegate onAdd"); + + if (mTabDelegate != null) { + mTabDelegate.onTabAdd(); + } + } + + @Override + public void onClick(TabsBarItem item) { + if (mTabDelegate != null) { + mTabDelegate.onTabSelect(item.getSession()); + } + } + + @Override + public void onClose(TabsBarItem item) { + if (mTabDelegate != null) { + mTabDelegate.onTabsClose(Collections.singletonList(item.getSession())); + } + if (mTabs.size() > 1) { + List latestTabs = SessionStore.get().getSessions(mPrivateMode); + if (latestTabs.size() != (mTabs.size() - 1) && latestTabs.size() > 0) { + item.attachToSession(latestTabs.get(0)); + return; + } + refreshTabs(); + } + } + }; + } +} 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/Windows.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/Windows.java index 2e258db486..b3809d8666 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); @@ -1382,6 +1382,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/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/tabs_bar.xml b/app/src/main/res/layout/tabs_bar.xml new file mode 100644 index 0000000000..cbdcdc8bb5 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tabs_bar_item.xml b/app/src/main/res/layout/tabs_bar_item.xml new file mode 100644 index 0000000000..12abf38212 --- /dev/null +++ b/app/src/main/res/layout/tabs_bar_item.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 0282b7be26..0d2ea932ef 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -115,6 +115,10 @@ 7dp 3dp + + 200dp + 450dp + 400dp 200dp