For #16132 - Revise multiselect mode UI

upstream-sync
ekager 4 years ago
parent 1f6f29ea7d
commit a8db85fc22

@ -141,7 +141,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? AddonInstallationDialogFragment != null
as? AddonInstallationDialogFragment != null
}
private fun showPermissionDialog(addon: Addon) {

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.tabtray
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
class MultiselectSelectionMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object BookmarkTabs : Item()
object DeleteTabs : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_multiselect_menu_item_bookmark),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.BookmarkTabs)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_multiselect_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.DeleteTabs)
}
)
}
}

@ -4,13 +4,20 @@
package org.mozilla.fenix.tabtray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection
@ -18,7 +25,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
import mozilla.components.browser.storage.sync.Tab as SyncTab
@ -29,13 +35,16 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
*/
@Suppress("TooManyFunctions")
interface TabTrayController {
fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed()
fun handleNewTabTapped(private: Boolean)
fun handleTabTrayDismissed()
fun handleTabSettingsClicked()
fun onShareTabsClicked(private: Boolean)
fun onSyncedTabClicked(syncTab: SyncTab)
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun onCloseAllTabsClicked(private: Boolean)
fun handleShareTabsOfTypeClicked(private: Boolean)
fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>)
fun handleSyncedTabClicked(syncTab: SyncTab)
fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>)
fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>)
fun handleCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean
fun onModeRequested(): TabTrayDialogFragmentState.Mode
fun handleAddSelectedTab(tab: Tab)
@ -68,8 +77,12 @@ class DefaultTabTrayController(
private val activity: HomeActivity,
private val profiler: Profiler?,
private val sessionManager: SessionManager,
private val browserStore: BrowserStore,
private val browsingModeManager: BrowsingModeManager,
private val tabCollectionStorage: TabCollectionStorage,
private val bookmarksStorage: BookmarksStorage,
private val scope: CoroutineScope,
private val tabsUseCases: TabsUseCases,
private val navController: NavController,
private val dismissTabTray: () -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
@ -77,10 +90,12 @@ class DefaultTabTrayController(
private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val showChooseCollectionDialog: (List<Session>) -> Unit,
private val showAddNewCollectionDialog: (List<Session>) -> Unit
private val showAddNewCollectionDialog: (List<Session>) -> Unit,
private val showUndoSnackbarForTabs: () -> Unit,
private val showBookmarksSnackbar: () -> Unit
) : TabTrayController {
override fun onNewTabTapped(private: Boolean) {
override fun handleNewTabTapped(private: Boolean) {
val startTime = profiler?.getProfilerTime()
browsingModeManager.mode = BrowsingMode.fromBoolean(private)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
@ -95,11 +110,11 @@ class DefaultTabTrayController(
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment())
}
override fun onTabTrayDismissed() {
override fun handleTabTrayDismissed() {
dismissTabTray()
}
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
override fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>) {
val sessionList = selectedTabs.map {
sessionManager.findSessionById(it.id) ?: return
}
@ -117,9 +132,19 @@ class DefaultTabTrayController(
}
}
override fun onShareTabsClicked(private: Boolean) {
val tabs = getListOfSessions(private)
override fun handleShareTabsOfTypeClicked(private: Boolean) {
val tabs = browserStore.state.getNormalOrPrivateTabs(private)
val data = tabs.map {
ShareData(url = it.content.url, title = it.content.title)
}
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
}
override fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
val data = selectedTabs.map {
ShareData(url = it.url, title = it.title)
}
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
@ -128,7 +153,40 @@ class DefaultTabTrayController(
navController.navigate(directions)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
override fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
selectedTabs.forEach {
scope.launch(IO) {
val shouldAddBookmark = bookmarksStorage.getBookmarksWithUrl(it.url)
.firstOrNull { it.url == it.url } == null
if (shouldAddBookmark) {
bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = it.url,
title = it.title,
position = null
)
}
}
}
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showBookmarksSnackbar()
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>) {
if (browserStore.state.normalTabs.size == selectedTabs.size) {
dismissTabTrayAndNavigateHome(HomeFragment.ALL_NORMAL_TABS)
} else {
selectedTabs.map { it.id }.let {
tabsUseCases.removeTabs(it)
}
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showUndoSnackbarForTabs()
}
}
override fun handleSyncedTabClicked(syncTab: SyncTab) {
activity.openToBrowserAndLoad(
searchTermOrURL = syncTab.active().url,
newTab = true,
@ -137,7 +195,7 @@ class DefaultTabTrayController(
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
override fun handleCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) {
HomeFragment.ALL_PRIVATE_TABS
} else {
@ -164,11 +222,6 @@ class DefaultTabTrayController(
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
private fun getListOfSessions(private: Boolean): List<Session> {
return sessionManager.sessionsOfType(private = private).toList()
}
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
return tabTrayDialogFragmentStore.state.mode
}

@ -30,6 +30,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
@ -71,7 +72,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
private val snackbarAnchor: View?
get() = if (tabTrayView.fabView.new_tab_button.isVisible ||
tabTrayView.mode != Mode.Normal) tabTrayView.fabView.new_tab_button
tabTrayView.mode != Mode.Normal
) tabTrayView.fabView.new_tab_button
/* During selection of the tabs to the collection, the FAB is not visible,
which leads to not attaching a needed AnchorView. That's why, we're not only checking, if it's not visible,
but also if we're not in a "Normal" mode, so after selecting tabs for a collection, we're pushing snackbar
@ -177,6 +179,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
}
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = activity as HomeActivity
@ -194,8 +197,12 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
activity = activity,
profiler = activity.components.core.engine.profiler,
sessionManager = activity.components.core.sessionManager,
browserStore = activity.components.core.store,
tabsUseCases = activity.components.useCases.tabsUseCases,
scope = lifecycleScope,
browsingModeManager = activity.browsingModeManager,
tabCollectionStorage = activity.components.core.tabCollectionStorage,
bookmarksStorage = activity.components.core.bookmarksStorage,
navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
@ -203,7 +210,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
tabTrayDialogFragmentStore = tabTrayDialogStore,
selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = ::showChooseCollectionDialog,
showAddNewCollectionDialog = ::showAddNewCollectionDialog
showAddNewCollectionDialog = ::showAddNewCollectionDialog,
showUndoSnackbarForTabs = ::showUndoSnackbarForTabs,
showBookmarksSnackbar = ::showBookmarksSnackbar
)
),
store = tabTrayDialogStore,
@ -267,6 +276,20 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
}
}
private fun showUndoSnackbarForTabs() {
lifecycleScope.allowUndo(
requireView().tabLayout,
getString(R.string.snackbar_message_tabs_closed),
getString(R.string.snackbar_deleted_undo),
{
requireComponents.useCases.tabsUseCases.undo.invoke()
},
operation = { },
elevation = ELEVATION,
anchorView = snackbarAnchor
)
}
private fun showUndoSnackbarForTab(sessionId: String) {
val store = requireComponents.core.store
val tab = requireComponents.core.store.state.findTab(sessionId) ?: return
@ -358,6 +381,26 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
}
}
private fun showBookmarksSnackbar() {
val snackbar = FenixSnackbar
.make(
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = false,
view = (view as View)
)
.setAnchorView(snackbarAnchor)
.setText(requireContext().getString(R.string.snackbar_message_bookmarks_saved))
.setAction(requireContext().getString(R.string.snackbar_message_bookmarks_view)) {
dismissAllowingStateLoss()
findNavController().navigate(
TabTrayDialogFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}
snackbar.view.elevation = ELEVATION
snackbar.show()
}
override fun onBackPressed(): Boolean {
if (!tabTrayView.onBackPressed()) {
dismiss()

@ -22,7 +22,22 @@ interface TabTrayInteractor {
/**
* Called when user clicks the share tabs button.
*/
fun onShareTabsClicked(private: Boolean)
fun onShareTabsOfTypeClicked(private: Boolean)
/**
* Called when user clicks button to share selected tabs in multiselect.
*/
fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>)
/**
* Called when user clicks bookmark button in menu to bookmark selected tabs in multiselect.
*/
fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>)
/**
* Called when user clicks delete button in menu to delete selected tabs in multiselect.
*/
fun onDeleteSelectedTabs(selectedTabs: Set<Tab>)
/**
* Called when user clicks the tab settings button.
@ -91,11 +106,11 @@ interface TabTrayInteractor {
@Suppress("TooManyFunctions")
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
override fun onNewTabTapped(private: Boolean) {
controller.onNewTabTapped(private)
controller.handleNewTabTapped(private)
}
override fun onTabTrayDismissed() {
controller.onTabTrayDismissed()
controller.handleTabTrayDismissed()
}
override fun onTabSettingsClicked() {
@ -106,20 +121,32 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.handleRecentlyClosedClicked()
}
override fun onShareTabsClicked(private: Boolean) {
controller.onShareTabsClicked(private)
override fun onShareTabsOfTypeClicked(private: Boolean) {
controller.handleShareTabsOfTypeClicked(private)
}
override fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
controller.handleShareSelectedTabsClicked(selectedTabs)
}
override fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
controller.handleBookmarkSelectedTabs(selectedTabs)
}
override fun onDeleteSelectedTabs(selectedTabs: Set<Tab>) {
controller.handleDeleteSelectedTabs(selectedTabs)
}
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
controller.onSaveToCollectionClicked(selectedTabs)
controller.handleSaveToCollectionClicked(selectedTabs)
}
override fun onCloseAllTabsClicked(private: Boolean) {
controller.onCloseAllTabsClicked(private)
controller.handleCloseAllTabsClicked(private)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
controller.onSyncedTabClicked(syncTab)
controller.handleSyncedTabClicked(syncTab)
}
override fun onBackPressed(): Boolean {

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.tabtray
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
class TabTrayItemMenu(
private val context: Context,
private val shouldShowSaveToCollection: () -> Boolean,
private val hasOpenTabs: () -> Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object ShareAllTabs : Item()
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_save),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.SaveToCollection)
}.apply { visible = shouldShowSaveToCollection },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_share),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.ShareAllTabs)
}.apply { visible = hasOpenTabs },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_tab_settings),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed)
onItemTapped.invoke(Item.CloseAllTabs)
}.apply { visible = hasOpenTabs }
)
}
}

@ -30,12 +30,11 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
@ -90,6 +89,9 @@ class TabTrayView(
private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null
private val multiselectSelectionMenu: MultiselectSelectionMenu
private var multiselectMenu: BrowserMenu? = null
private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
@ -230,7 +232,7 @@ class TabTrayView(
hasOpenTabs = checkOpenTabs
) {
when (it) {
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsOfTypeClicked(
isPrivateModeSelected
)
is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked()
@ -242,18 +244,30 @@ class TabTrayView(
}
}
multiselectSelectionMenu = MultiselectSelectionMenu(
context = view.context
) {
when (it) {
is MultiselectSelectionMenu.Item.BookmarkTabs -> interactor.onBookmarkSelectedTabs(
mode.selectedItems
)
is MultiselectSelectionMenu.Item.DeleteTabs -> interactor.onDeleteSelectedTabs(
mode.selectedItems
)
}
}
view.tab_tray_overflow.setOnClickListener {
components.analytics.metrics.track(Event.TabsTrayMenuOpened)
menu = tabTrayItemMenu.menuBuilder.build(container.context)
menu?.show(it)
?.also { pu ->
(pu.contentView as? CardView)?.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
R.color.foundation_normal_theme
)
menu?.show(it)?.also { popupMenu ->
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
R.color.foundation_normal_theme
)
}
)
}
}
adjustNewTabButtonsForNormalMode()
@ -469,6 +483,8 @@ class TabTrayView(
fabView.new_tab_button.isVisible = false
view.tab_tray_new_tab.isVisible = false
view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.share_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.menu_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.multiselect_title.text = view.context.getString(
R.string.tab_tray_multi_select_title,
@ -477,6 +493,20 @@ class TabTrayView(
view.collect_multi_select.setOnClickListener {
interactor.onSaveToCollectionClicked(state.mode.selectedItems)
}
view.share_multi_select.setOnClickListener {
interactor.onShareSelectedTabsClicked(state.mode.selectedItems)
}
view.menu_multi_select.setOnClickListener {
multiselectMenu = multiselectSelectionMenu.menuBuilder.build(container.context)
multiselectMenu?.show(it)?.also { popupMenu ->
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
R.color.foundation_normal_theme
)
)
}
}
view.exit_multi_select.setOnClickListener {
interactor.onBackPressed()
}
@ -544,6 +574,8 @@ class TabTrayView(
private fun toggleUIMultiselect(multiselect: Boolean) {
view.multiselect_title.isVisible = multiselect
view.collect_multi_select.isVisible = multiselect
view.share_multi_select.isVisible = multiselect
view.menu_multi_select.isVisible = multiselect
view.exit_multi_select.isVisible = multiselect
view.topBar.setBackgroundColor(
@ -707,9 +739,7 @@ class TabTrayView(
// We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`.
return if (view.context.settings().listTabView) {
selectedBrowserTabIndex +
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount
selectedBrowserTabIndex + collectionsButtonAdapter.itemCount + syncedTabsController.adapter.itemCount
} else {
selectedBrowserTabIndex
}
@ -719,75 +749,18 @@ class TabTrayView(
private const val TAB_COUNT_SHOW_CFR = 6
private const val DEFAULT_TAB_ID = 0
private const val PRIVATE_TAB_ID = 1
// Minimum number of list items for which to show the tabs tray as expanded.
private const val EXPAND_AT_LIST_SIZE = 4
// Minimum number of grid items for which to show the tabs tray as expanded.
private const val EXPAND_AT_GRID_SIZE = 3
private const val SLIDE_OFFSET = 0
private const val SELECTION_DELAY = 500
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
private const val COLUMN_WIDTH_DP = 180
// The remaining padding offset needed to provide a 16dp column spacing between the grid items.
const val GRID_ITEM_PARENT_PADDING = 8
}
}
class TabTrayItemMenu(
private val context: Context,
private val shouldShowSaveToCollection: () -> Boolean,
private val hasOpenTabs: () -> Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object ShareAllTabs : Item()
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_save),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.SaveToCollection)
}.apply { visible = shouldShowSaveToCollection },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_share),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.ShareAllTabs)
}.apply { visible = hasOpenTabs },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_tab_settings),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed)
onItemTapped.invoke(Item.CloseAllTabs)
}.apply { visible = hasOpenTabs }
)
}
}

@ -23,14 +23,13 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@+id/topBar"/>
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
<TextView
android:id="@+id/tab_tray_empty_view"
@ -76,7 +75,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp"
android:textSize="20sp"
app:fontFamily="@font/metropolis_semibold"
android:focusableInTouchMode="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
@ -86,34 +86,12 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
<TextView
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/tabstray_multiselect_items" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="80dp"
app:tabMaxWidth="0dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -121,6 +99,7 @@
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
@ -150,8 +129,8 @@
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme"
app:srcCompat="@drawable/ic_new" />
app:srcCompat="@drawable/ic_new"
app:tint="@color/primary_text_normal_theme" />
<ImageButton
android:id="@+id/tab_tray_overflow"
@ -161,11 +140,11 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:tint="@color/tab_tray_heading_icon_menu_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" />
app:srcCompat="@drawable/ic_menu"
app:tint="@color/tab_tray_heading_icon_menu_normal_theme" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View

@ -2,10 +2,7 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<com.google.android.material.button.MaterialButton
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/NeutralButton"
android:layout_margin="8dp"
android:text="@string/save_to_collection"
app:icon="@drawable/ic_tab_collection" />
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/NeutralButton"
android:layout_margin="8dp"
android:text="@string/tabs_tray_select_tabs" />

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageButton
android:id="@+id/collect_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/share_multi_select"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_tab_collection"
app:tint="@color/contrast_text_normal_theme" />
<ImageButton
android:id="@+id/share_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_multiselect_share_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/menu_multi_select"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_hollow_share"
app:tint="@color/contrast_text_normal_theme" />
<ImageButton
android:id="@+id/menu_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_multiselect_menu_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_menu"
app:tint="@color/contrast_text_normal_theme" />
</merge>

@ -584,8 +584,18 @@
<string name="tab_tray_menu_home">Go home</string>
<!-- Shortcut action to toggle private mode -->
<string name="tab_tray_menu_toggle">Toggle tab mode</string>
<!-- Text shown in the multiselect menu for bookmarking selected tabs. -->
<string name="tab_tray_multiselect_menu_item_bookmark">Bookmark</string>
<!-- Text shown in the multiselect menu for closing selected tabs. -->
<string name="tab_tray_multiselect_menu_item_close">Close</string>
<!-- Content description for tabs tray multiselect share button -->
<string name="tab_tray_multiselect_share_content_description">Share selected tabs</string>
<!-- Content description for tabs tray multiselect menu -->
<string name="tab_tray_multiselect_menu_content_description">Selected tabs menu</string>
<!-- Content description (not visible, for screen readers etc.): Removes tab from collection button. Removes the selected tab from collection when pressed -->
<string name="remove_tab_from_collection">Remove tab from collection</string>
<!-- Text for button to enter multiselect mode in tabs tray -->
<string name="tabs_tray_select_tabs">Select tabs</string>
<!-- Content description (not visible, for screen readers etc.): Close tab button. Closes the current session when pressed -->
<string name="close_tab">Close tab</string>
<!-- Content description (not visible, for screen readers etc.): Close tab <title> button. First parameter is tab title -->
@ -950,6 +960,12 @@
<string name="snackbar_tab_closed">Tab closed</string>
<!-- Text shown in snackbar when user closes all tabs -->
<string name="snackbar_tabs_closed">Tabs closed</string>
<!-- Text shown in snackbar when user closes tabs -->
<string name="snackbar_message_tabs_closed">Tabs closed!</string>
<!-- Text shown in snackbar when user bookmarks a list of tabs -->
<string name="snackbar_message_bookmarks_saved">Bookmarks saved!</string>
<!-- Text shown in snackbar action for viewing bookmarks -->
<string name="snackbar_message_bookmarks_view">View</string>
<!-- Text shown in snackbar when user adds a site to top sites -->
<string name="snackbar_added_to_top_sites">Added to top sites!</string>
<!-- Text shown in snackbar when user closes a private tab -->

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -16,9 +17,14 @@ import io.mockk.slot
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
@ -40,6 +46,14 @@ class DefaultTabTrayControllerTest {
private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true)
var store = BrowserStore(
BrowserState(
tabs = listOf(
createTab(url = "http://firefox.com", id = "5678"),
createTab(url = "http://mozilla.org", id = "1234")
), selectedTabId = "1234"
)
)
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
@ -47,11 +61,15 @@ class DefaultTabTrayControllerTest {
private val showChooseCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
private val showAddNewCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val bookmarksStorage: BookmarksStorage = mockk(relaxed = true)
private val tabCollection: TabCollection = mockk()
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
private val currentDestination: NavDestination = mockk(relaxed = true)
private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true)
private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
private val showUndoSnackbarForTabs: (() -> Unit) = mockk(relaxed = true)
private val showBookmarksSavedSnackbar: (() -> Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultTabTrayController
@ -87,16 +105,22 @@ class DefaultTabTrayControllerTest {
activity = activity,
profiler = profiler,
sessionManager = sessionManager,
browserStore = store,
browsingModeManager = browsingModeManager,
tabCollectionStorage = tabCollectionStorage,
bookmarksStorage = bookmarksStorage,
scope = TestCoroutineScope(),
navController = navController,
tabsUseCases = tabsUseCases,
dismissTabTray = dismissTabTray,
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = registerCollectionStorageObserver,
tabTrayDialogFragmentStore = tabTrayFragmentStore,
selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = showChooseCollectionDialog,
showAddNewCollectionDialog = showAddNewCollectionDialog
showAddNewCollectionDialog = showAddNewCollectionDialog,
showUndoSnackbarForTabs = showUndoSnackbarForTabs,
showBookmarksSnackbar = showBookmarksSavedSnackbar
)
}
@ -113,7 +137,7 @@ class DefaultTabTrayControllerTest {
@Test
fun onNewTabTapped() {
controller.onNewTabTapped(private = false)
controller.handleNewTabTapped(private = false)
verifyOrder {
browsingModeManager.mode = BrowsingMode.fromBoolean(false)
@ -125,7 +149,7 @@ class DefaultTabTrayControllerTest {
dismissTabTray()
}
controller.onNewTabTapped(private = true)
controller.handleNewTabTapped(private = true)
verifyOrder {
browsingModeManager.mode = BrowsingMode.fromBoolean(true)
@ -140,7 +164,7 @@ class DefaultTabTrayControllerTest {
@Test
fun onTabTrayDismissed() {
controller.onTabTrayDismissed()
controller.handleTabTrayDismissed()
verify {
dismissTabTray()
@ -152,7 +176,7 @@ class DefaultTabTrayControllerTest {
val navDirectionsSlot = slot<NavDirections>()
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
controller.onShareTabsClicked(private = false)
controller.handleShareTabsOfTypeClicked(private = false)
verify {
navController.navigate(capture(navDirectionsSlot))
@ -164,7 +188,7 @@ class DefaultTabTrayControllerTest {
@Test
fun onCloseAllTabsClicked() {
controller.onCloseAllTabsClicked(private = false)
controller.handleCloseAllTabsClicked(private = false)
verify {
dismissTabTrayAndNavigateHome(any())
@ -173,7 +197,7 @@ class DefaultTabTrayControllerTest {
@Test
fun onSyncedTabClicked() {
controller.onSyncedTabClicked(mockk(relaxed = true))
controller.handleSyncedTabClicked(mockk(relaxed = true))
verify {
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
@ -242,13 +266,53 @@ class DefaultTabTrayControllerTest {
fun onSaveToCollectionClicked() {
val tab = Tab("1234", "mozilla.org")
controller.onSaveToCollectionClicked(setOf(tab))
controller.handleSaveToCollectionClicked(setOf(tab))
verify {
registerCollectionStorageObserver()
showChooseCollectionDialog(listOf(session))
}
}
@Test
fun handleShareSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val navDirectionsSlot = slot<NavDirections>()
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
controller.handleShareSelectedTabsClicked(setOf(tab))
verify {
navController.navigate(capture(navDirectionsSlot))
}
assertTrue(navDirectionsSlot.isCaptured)
assertEquals(R.id.action_global_shareFragment, navDirectionsSlot.captured.actionId)
}
@Test
fun handleDeleteSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
controller.handleDeleteSelectedTabs(setOf(tab))
verify {
tabsUseCases.removeTabs(listOf(tab.id))
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showUndoSnackbarForTabs()
}
}
@Test
fun handleBookmarkSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
coEvery { bookmarksStorage.getBookmarksWithUrl("mozilla.org") } returns listOf()
controller.handleBookmarkSelectedTabs(setOf(tab))
verify {
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showBookmarksSavedSnackbar()
}
}
@Test
fun handleSetUpAutoCloseTabsClicked() {
controller.handleSetUpAutoCloseTabsClicked()

@ -13,13 +13,40 @@ class TabTrayFragmentInteractorTest {
private val controller = mockk<TabTrayController>(relaxed = true)
private val interactor = TabTrayFragmentInteractor(controller)
@Test
fun onShareSelectedTabsClicked() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onShareSelectedTabsClicked(selectedTabs)
verify { controller.handleShareSelectedTabsClicked(selectedTabs) }
}
@Test
fun onBookmarkSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onBookmarkSelectedTabs(selectedTabs)
verify { controller.handleBookmarkSelectedTabs(selectedTabs) }
}
@Test
fun onDeleteSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onDeleteSelectedTabs(selectedTabs)
verify { controller.handleDeleteSelectedTabs(selectedTabs) }
}
@Test
fun onNewTabTapped() {
interactor.onNewTabTapped(private = true)
verify { controller.onNewTabTapped(true) }
verify { controller.handleNewTabTapped(true) }
interactor.onNewTabTapped(private = false)
verify { controller.onNewTabTapped(false) }
verify { controller.handleNewTabTapped(false) }
}
@Test
@ -34,38 +61,38 @@ class TabTrayFragmentInteractorTest {
@Test
fun onTabTrayDismissed() {
interactor.onTabTrayDismissed()
verify { controller.onTabTrayDismissed() }
verify { controller.handleTabTrayDismissed() }
}
@Test
fun onShareTabsClicked() {
interactor.onShareTabsClicked(private = true)
verify { controller.onShareTabsClicked(true) }
interactor.onShareTabsOfTypeClicked(private = true)
verify { controller.handleShareTabsOfTypeClicked(true) }
interactor.onShareTabsClicked(private = false)
verify { controller.onShareTabsClicked(false) }
interactor.onShareTabsOfTypeClicked(private = false)
verify { controller.handleShareTabsOfTypeClicked(false) }
}
@Test
fun onSaveToCollectionClicked() {
val tab = Tab("1234", "mozilla.org")
interactor.onSaveToCollectionClicked(setOf(tab))
verify { controller.onSaveToCollectionClicked(setOf(tab)) }
verify { controller.handleSaveToCollectionClicked(setOf(tab)) }
}
@Test
fun onCloseAllTabsClicked() {
interactor.onCloseAllTabsClicked(private = false)
verify { controller.onCloseAllTabsClicked(false) }
verify { controller.handleCloseAllTabsClicked(false) }
interactor.onCloseAllTabsClicked(private = true)
verify { controller.onCloseAllTabsClicked(true) }
verify { controller.handleCloseAllTabsClicked(true) }
}
@Test
fun onSyncedTabClicked() {
interactor.onSyncedTabClicked(mockk(relaxed = true))
verify { controller.onSyncedTabClicked(any()) }
verify { controller.handleSyncedTabClicked(any()) }
}
@Test

Loading…
Cancel
Save