Closes #18513: Re-add menu to tabs tray (#18756)

upstream-sync
Roger Yang 3 years ago committed by GitHub
parent a443509c8b
commit 01568d5859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,93 @@
/* 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.tabstray
import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.HomeFragment
/**
* For interacting with UI that is specifically for [TabsTrayFragment[]] and other browser
* tab tray views.
*/
interface NavigationInteractor {
/**
* Called when tab tray should be dismissed.
*/
fun onTabTrayDismissed()
/**
* Called when user clicks the share tabs button.
*/
fun onShareTabsOfTypeClicked(private: Boolean)
/**
* Called when user clicks the tab settings button.
*/
fun onTabSettingsClicked()
/**
* Called when user clicks the close all tabs button.
*/
fun onCloseAllTabsClicked(private: Boolean)
/**
* Called when user clicks the recently closed tabs menu button.
*/
fun onOpenRecentlyClosedClicked()
}
/**
* A default implementation of [NavigationInteractor].
*/
class DefaultNavigationInteractor(
private val browserStore: BrowserStore,
private val navController: NavController,
private val metrics: MetricController,
private val dismissTabTray: () -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit
) : NavigationInteractor {
override fun onTabTrayDismissed() {
dismissTabTray()
}
override fun onTabSettingsClicked() {
navController.navigate(TabsTrayFragmentDirections.actionGlobalTabSettingsFragment())
}
override fun onOpenRecentlyClosedClicked() {
navController.navigate(TabsTrayFragmentDirections.actionGlobalRecentlyClosed())
metrics.track(Event.RecentlyClosedTabsOpened)
}
override fun onShareTabsOfTypeClicked(private: Boolean) {
val tabs = browserStore.state.getNormalOrPrivateTabs(private)
val data = tabs.map {
ShareData(url = it.content.url, title = it.content.title)
}
val directions = TabsTrayFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) {
HomeFragment.ALL_PRIVATE_TABS
} else {
HomeFragment.ALL_NORMAL_TABS
}
dismissTabTrayAndNavigateHome(sessionsToClose)
}
}

@ -10,9 +10,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.cardview.widget.CardView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.component_tabstray2.*
import kotlinx.android.synthetic.main.component_tabstray2.view.*
import org.mozilla.fenix.HomeActivity
@ -21,8 +25,12 @@ import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.ui.tabcounter.TabCounter
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.RemoveTabUseCaseWrapper
@ -31,7 +39,14 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
private lateinit var navigationInteractor: NavigationInteractor
private val tabLayout: TabLayout? get() =
view?.tab_layout
private val isPrivateModeSelected: Boolean get() =
tabLayout?.selectedTabPosition == TrayPagerAdapter.POSITION_PRIVATE_TABS
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
@ -64,15 +79,26 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
val containerView = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
val view: View = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_tabstray2, containerView as ViewGroup, true)
val activity = activity as HomeActivity
behavior = BottomSheetBehavior.from(view.tab_wrapper)
navigationInteractor =
DefaultNavigationInteractor(
browserStore = activity.components.core.store,
navController = findNavController(),
metrics = activity.components.analytics.metrics,
dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome
)
return containerView
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupMenu(view)
val browserTrayInteractor = DefaultBrowserTrayInteractor(
this,
@ -148,4 +174,47 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
isUserInputEnabled = false
}
}
private fun setupMenu(view: View) {
view.tab_tray_overflow.setOnClickListener { anchor ->
val tabTrayItemMenu =
TabsTrayMenu(
context = view.context,
browserStore = requireComponents.core.store,
tabLayout = tab_layout
) {
when (it) {
is TabsTrayMenu.Item.ShareAllTabs ->
navigationInteractor.onShareTabsOfTypeClicked(isPrivateModeSelected)
is TabsTrayMenu.Item.OpenTabSettings ->
navigationInteractor.onTabSettingsClicked()
is TabsTrayMenu.Item.CloseAllTabs ->
navigationInteractor.onCloseAllTabsClicked(isPrivateModeSelected)
is TabsTrayMenu.Item.OpenRecentlyClosed ->
navigationInteractor.onOpenRecentlyClosedClicked()
is TabsTrayMenu.Item.SelectTabs ->
{ /* TODO implement when mulitiselect call is available */ }
}
}
requireComponents.analytics.metrics.track(Event.TabsTrayMenuOpened)
val menu = tabTrayItemMenu.menuBuilder.build(view.context)
menu.show(anchor).also { popupMenu ->
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
R.color.foundation_normal_theme
)
)
}
}
}
private val homeViewModel: HomeScreenViewModel by activityViewModels()
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
homeViewModel.sessionToDelete = sessionId
val directions = NavGraphDirections.actionGlobalHome()
findNavController().navigate(directions)
dismissAllowingStateLoss()
}
}

@ -0,0 +1,89 @@
/* 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.tabstray
import android.content.Context
import com.google.android.material.tabs.TabLayout
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.tabstray.ext.isNormalModeSelected
import org.mozilla.fenix.tabstray.ext.isPrivateModeSelected
class TabsTrayMenu(
private val context: Context,
browserStore: BrowserStore,
private val tabLayout: TabLayout,
private val onItemTapped: (Item) -> Unit = {}
) {
private val checkOpenTabs =
when {
tabLayout.isNormalModeSelected() ->
browserStore.state.normalTabs.isNotEmpty()
tabLayout.isPrivateModeSelected() ->
browserStore.state.privateTabs.isNotEmpty()
else ->
false
}
private val shouldShowSelectOrShare = { tabLayout.isNormalModeSelected() && checkOpenTabs }
sealed class Item {
object ShareAllTabs : Item()
object OpenTabSettings : Item()
object SelectTabs : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tabs_tray_select_tabs),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.SelectTabs)
}.apply { visible = shouldShowSelectOrShare },
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 = shouldShowSelectOrShare },
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 = { checkOpenTabs } }
)
}
}

@ -0,0 +1,16 @@
/* 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.tabstray.ext
import com.google.android.material.tabs.TabLayout
import org.mozilla.fenix.tabstray.TrayPagerAdapter
fun TabLayout.isNormalModeSelected(): Boolean {
return selectedTabPosition == TrayPagerAdapter.POSITION_NORMAL_TABS
}
fun TabLayout.isPrivateModeSelected(): Boolean {
return selectedTabPosition == TrayPagerAdapter.POSITION_PRIVATE_TABS
}

@ -0,0 +1,106 @@
/* 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.tabstray
import androidx.navigation.NavController
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.components.metrics.MetricController
class NavigationInteractorTest {
private lateinit var store: BrowserStore
private lateinit var navigationInteractor: NavigationInteractor
private val testTab: TabSessionState = createTab(url = "https://mozilla.org")
private val navController: NavController = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val dismissTabTray: () -> Unit = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: (String) -> Unit = mockk(relaxed = true)
@Before
fun setup() {
store = BrowserStore(initialState = BrowserState(tabs = listOf(testTab)))
navigationInteractor = DefaultNavigationInteractor(
store,
navController,
metrics,
dismissTabTray,
dismissTabTrayAndNavigateHome
)
}
@Test
fun `navigation interactor calls the overridden functions`() {
var tabTrayDismissed = false
var tabSettingsClicked = false
var openRecentlyClosedClicked = false
var shareTabsOfTypeClicked = false
var closeAllTabsClicked = false
class TestNavigationInteractor : NavigationInteractor {
override fun onTabTrayDismissed() {
tabTrayDismissed = true
}
override fun onTabSettingsClicked() {
tabSettingsClicked = true
}
override fun onOpenRecentlyClosedClicked() {
openRecentlyClosedClicked = true
}
override fun onShareTabsOfTypeClicked(private: Boolean) {
shareTabsOfTypeClicked = true
}
override fun onCloseAllTabsClicked(private: Boolean) {
closeAllTabsClicked = true
}
}
val navigationInteractor: NavigationInteractor = TestNavigationInteractor()
navigationInteractor.onTabTrayDismissed()
assert(tabTrayDismissed)
navigationInteractor.onTabSettingsClicked()
assert(tabSettingsClicked)
navigationInteractor.onOpenRecentlyClosedClicked()
assert(openRecentlyClosedClicked)
navigationInteractor.onShareTabsOfTypeClicked(true)
assert(shareTabsOfTypeClicked)
navigationInteractor.onCloseAllTabsClicked(true)
assert(closeAllTabsClicked)
}
@Test
fun `onTabTrayDismissed calls dismissTabTray on DefaultNaviationInteractor`() {
navigationInteractor.onTabTrayDismissed()
verify(exactly = 1) { dismissTabTray() }
}
@Test
fun `onTabSettingsClicked calls navigation on DefaultNaviationInteractor`() {
navigationInteractor.onTabSettingsClicked()
verify(exactly = 1) { navController.navigate(TabsTrayFragmentDirections.actionGlobalTabSettingsFragment()) }
}
@Test
fun `onOpenRecentlyClosedClicked calls navigation on DefaultNaviationInteractor`() {
navigationInteractor.onOpenRecentlyClosedClicked()
verify(exactly = 1) { navController.navigate(TabsTrayFragmentDirections.actionGlobalRecentlyClosed()) }
}
@Test
fun `onCloseAllTabsClicked calls navigation on DefaultNaviationInteractor`() {
navigationInteractor.onCloseAllTabsClicked(false)
verify(exactly = 1) { dismissTabTrayAndNavigateHome(any()) }
}
}
Loading…
Cancel
Save