Commit a8db85fc authored by ekager's avatar ekager
Browse files

For #16132 - Revise multiselect mode UI

parent 1f6f29ea
......@@ -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) {
......
/* 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 {
......
/* 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(