1
0
Fork 0

For #10374 - Adds quick contextual menu to tab switcher

Co-authored-by: person808 <kainaluh808@gmail.com>
master
Jeff Boek 2020-06-10 15:34:26 -07:00 committed by Kainalu Hagiwara
parent 137d66a511
commit cd3b6181b3
13 changed files with 255 additions and 8 deletions

View File

@ -96,6 +96,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
@ -218,6 +219,32 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
sharedViewModel = sharedViewModel,
onTabCounterClicked = {
TabTrayDialogFragment.show(parentFragmentManager)
},
onCloseTab = {
val snapshot = sessionManager.createSessionSnapshot(it)
val state = snapshot.engineSession?.saveState()
val isSelected =
it.id == context.components.core.store.state.selectedTabId ?: false
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
},
operation = { }
)
}
)

View File

@ -12,6 +12,10 @@ open class BrowserInteractor(
browserToolbarController.handleTabCounterClick()
}
override fun onTabCounterMenuItemTapped(item: TabCounterMenuItem) {
browserToolbarController.handleTabCounterItemInteraction(item)
}
override fun onBrowserToolbarPaste(text: String) {
browserToolbarController.handleToolbarPaste(text)
}

View File

@ -22,11 +22,13 @@ import mozilla.components.browser.state.selector.findTab
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar
@ -37,6 +39,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
@ -52,11 +55,12 @@ interface BrowserToolbarController {
fun handleToolbarItemInteraction(item: ToolbarMenu.Item)
fun handleToolbarClick()
fun handleTabCounterClick()
fun handleTabCounterItemInteraction(item: TabCounterMenuItem)
fun handleBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>)
fun handleReaderModePressed(enabled: Boolean)
}
@Suppress("LargeClass", "TooManyFunctions")
@SuppressWarnings("LargeClass", "TooManyFunctions")
class DefaultBrowserToolbarController(
private val activity: Activity,
private val navController: NavController,
@ -73,7 +77,8 @@ class DefaultBrowserToolbarController(
private val tabCollectionStorage: TabCollectionStorage,
private val topSiteStorage: TopSiteStorage,
private val sharedViewModel: SharedViewModel,
private val onTabCounterClicked: () -> Unit
private val onTabCounterClicked: () -> Unit,
private val onCloseTab: (Session) -> Unit
) : BrowserToolbarController {
private val currentSession
@ -133,6 +138,32 @@ class DefaultBrowserToolbarController(
}
}
override fun handleTabCounterItemInteraction(item: TabCounterMenuItem) {
val tabUseCases = activity.components.useCases.tabsUseCases
when (item) {
is TabCounterMenuItem.CloseTab -> {
activity.components.core.sessionManager.selectedSession?.let {
// When closing the last tab we must show the undo snackbar in the home fragment
if (activity.components.core.sessionManager.sessionsOfType(it.private)
.count() == 1
) {
// The tab tray always returns to normal mode so do that here too
(activity as HomeActivity).browsingModeManager.mode = BrowsingMode.Normal
navController.navigate(BrowserFragmentDirections.actionGlobalHome(it.id))
} else {
onCloseTab.invoke(it)
tabUseCases.removeTab.invoke(it)
}
}
}
is TabCounterMenuItem.NewTab -> {
(activity as HomeActivity).browsingModeManager.mode =
BrowsingMode.fromBoolean(item.isPrivate)
navController.popBackStack(R.id.homeFragment, false)
}
}
}
override fun handleBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>) {
lowPrioHighlightItems.forEach {
when (it) {

View File

@ -49,6 +49,7 @@ interface BrowserToolbarViewInteractor {
fun onBrowserToolbarClicked()
fun onBrowserToolbarMenuItemTapped(item: ToolbarMenu.Item)
fun onTabCounterClicked()
fun onTabCounterMenuItemTapped(item: TabCounterMenuItem)
fun onBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>)
fun onScrolled(offset: Int)
fun onReaderModePressed(enabled: Boolean)

View File

@ -0,0 +1,6 @@
package org.mozilla.fenix.components.toolbar
sealed class TabCounterMenuItem {
object CloseTab : TabCounterMenuItem()
class NewTab(val isPrivate: Boolean) : TabCounterMenuItem()
}

View File

@ -4,13 +4,20 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.toolbar.Toolbar
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
/**
@ -19,6 +26,7 @@ import java.lang.ref.WeakReference
class TabCounterToolbarButton(
private val sessionManager: SessionManager,
private val isPrivate: Boolean,
private val onItemTapped: (TabCounterMenuItem) -> Unit = {},
private val showTabs: () -> Unit
) : Toolbar.Action {
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
@ -32,6 +40,11 @@ class TabCounterToolbarButton(
showTabs.invoke()
}
setOnLongClickListener {
getTabContextMenu(it.context).show(it)
true
}
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setCount(sessionManager.sessionsOfType(private = isPrivate).count())
@ -62,6 +75,38 @@ class TabCounterToolbarButton(
}
}
private fun getTabContextMenu(context: Context): BrowserMenu {
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
val menuItems = listOf(
BrowserMenuImageText(
label = context.getString(R.string.close_tab),
imageResource = R.drawable.ic_close,
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
onItemTapped(TabCounterMenuItem.CloseTab)
},
BrowserMenuDivider(),
BrowserMenuImageText(
label = context.getString(R.string.browser_menu_new_tab),
imageResource = R.drawable.ic_new,
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
onItemTapped(TabCounterMenuItem.NewTab(false))
},
BrowserMenuImageText(
label = context.getString(R.string.home_screen_shortcut_open_new_private_tab_2),
imageResource = R.drawable.ic_private_browsing,
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
onItemTapped(TabCounterMenuItem.NewTab(true))
}
)
return BrowserMenuBuilder(menuItems).build(context)
}
private val sessionManagerObserver = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) {
updateCount()

View File

@ -132,7 +132,10 @@ class DefaultToolbarIntegration(
)
}
val tabsAction = TabCounterToolbarButton(sessionManager, isPrivate) {
val onTabCounterMenuItemTapped = { item: TabCounterMenuItem ->
interactor.onTabCounterMenuItemTapped(item)
}
val tabsAction = TabCounterToolbarButton(sessionManager, isPrivate, onTabCounterMenuItemTapped) {
toolbar.hideKeyboard()
interactor.onTabCounterClicked()
}

View File

@ -36,6 +36,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
@ -49,6 +50,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
@ -95,6 +99,7 @@ import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import kotlin.math.abs
@ -106,6 +111,8 @@ class HomeFragment : Fragment() {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
}
private val args by navArgs<HomeFragmentArgs>()
private val snackbarAnchorView: View?
get() {
return if (requireContext().settings().shouldUseBottomToolbar) {
@ -306,6 +313,10 @@ class HomeFragment : Fragment() {
}
createHomeMenu(requireContext(), WeakReference(view.menuButton))
view.tab_button.setOnLongClickListener {
createTabCounterMenu(requireContext()).show(view.tab_button)
true
}
view.menuButton.setColorFilter(
ContextCompat.getColor(
@ -453,7 +464,40 @@ class HomeFragment : Fragment() {
if (browsingModeManager.mode == BrowsingMode.Private) {
activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
}
hideToolbar()
args.sessionToDelete?.also {
sessionManager.findSessionById(it)?.let { session ->
val snapshot = sessionManager.createSessionSnapshot(session)
val state = snapshot.engineSession?.saveState()
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
},
operation = { },
anchorView = snackbarAnchorView
)
requireComponents.useCases.tabsUseCases.removeTab.invoke(session)
}
}
}
override fun onPause() {
@ -545,6 +589,31 @@ class HomeFragment : Fragment() {
)
}
private fun createTabCounterMenu(context: Context): BrowserMenu {
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
val isPrivate = (activity as HomeActivity).browsingModeManager.mode == BrowsingMode.Private
val menuItems = listOf(
BrowserMenuImageText(
label = context.getString(if (isPrivate) {
R.string.browser_menu_new_tab
} else {
R.string.home_screen_shortcut_open_new_private_tab_2
}),
imageResource = if (isPrivate) {
R.drawable.ic_new
} else {
R.drawable.ic_private_browsing
},
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
(activity as HomeActivity).browsingModeManager.mode =
BrowsingMode.fromBoolean(!isPrivate)
}
)
return BrowserMenuBuilder(menuItems).build(context)
}
@SuppressWarnings("ComplexMethod", "LongMethod")
private fun createHomeMenu(context: Context, menuButtonView: WeakReference<MenuButton>) =
HomeMenu(

View File

@ -163,6 +163,7 @@ class TabTrayView(
updateState(view.context.components.core.store.state)
}
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/ }
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/ }

View File

@ -66,6 +66,11 @@
app:destination="@id/browserFragment"
app:exitAnim="@anim/zoom_in_fade"
app:popEnterAnim="@anim/zoom_out_fade" />
<argument
android:name="session_to_delete"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<fragment

View File

@ -156,4 +156,5 @@
<dimen name="saved_logins_detail_menu_vertical_padding">5dp</dimen>
<dimen name="saved_logins_end_icon_drawable_padding">16dp</dimen>
</resources>

View File

@ -24,6 +24,14 @@ class BrowserInteractorTest {
verify { browserToolbarController.handleTabCounterClick() }
}
@Test
fun onTabCounterMenuItemTapped() {
val item: TabCounterMenuItem = mockk()
interactor.onTabCounterMenuItemTapped(item)
verify { browserToolbarController.handleTabCounterItemInteraction(item) }
}
@Test
fun onBrowserToolbarPaste() {
val pastedText = "Mozilla"

View File

@ -36,6 +36,7 @@ import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@ -45,6 +46,9 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.Analytics
@ -113,7 +117,8 @@ class DefaultBrowserToolbarControllerTest {
readerModeController = readerModeController,
sessionManager = mockk(),
sharedViewModel = mockk(),
onTabCounterClicked = { }
onTabCounterClicked = { },
onCloseTab = {}
)
mockkStatic(
@ -207,7 +212,8 @@ class DefaultBrowserToolbarControllerTest {
readerModeController = mockk(),
sessionManager = mockk(),
sharedViewModel = mockk(),
onTabCounterClicked = { }
onTabCounterClicked = { },
onCloseTab = { }
)
controller.handleBrowserMenuDismissed(itemList)
@ -371,7 +377,8 @@ class DefaultBrowserToolbarControllerTest {
readerModeController = mockk(),
sessionManager = mockk(),
sharedViewModel = mockk(),
onTabCounterClicked = { }
onTabCounterClicked = { },
onCloseTab = { }
)
controller.ioScope = this
@ -496,7 +503,8 @@ class DefaultBrowserToolbarControllerTest {
readerModeController = mockk(),
sessionManager = mockk(),
sharedViewModel = mockk(),
onTabCounterClicked = { }
onTabCounterClicked = { },
onCloseTab = { }
)
val sessionManager: SessionManager = mockk(relaxed = true)
@ -536,7 +544,8 @@ class DefaultBrowserToolbarControllerTest {
readerModeController = mockk(),
sessionManager = mockk(),
sharedViewModel = mockk(),
onTabCounterClicked = { }
onTabCounterClicked = { },
onCloseTab = { }
)
controller.handleToolbarItemInteraction(item)
@ -556,4 +565,41 @@ class DefaultBrowserToolbarControllerTest {
controller.handleToolbarItemInteraction(item)
verify { readerModeController.hideReaderView() }
}
@Test
fun handleToolbarCloseTabPress() {
val tabsUseCases: TabsUseCases = mockk(relaxed = true)
val removeTabUseCase: TabsUseCases.RemoveTabUseCase = mockk(relaxed = true)
val item = TabCounterMenuItem.CloseTab
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
every { tabsUseCases.removeTab } returns removeTabUseCase
controller.handleTabCounterItemInteraction(item)
verify { removeTabUseCase.invoke(currentSession) }
}
@Test
fun handleToolbarNewTabPress() {
val browsingModeManager: BrowsingModeManager = DefaultBrowsingModeManager(BrowsingMode.Private) {}
val item = TabCounterMenuItem.NewTab(false)
every { activity.browsingModeManager } returns browsingModeManager
controller.handleTabCounterItemInteraction(item)
assertEquals(BrowsingMode.Normal, activity.browsingModeManager.mode)
verify { navController.popBackStack(R.id.homeFragment, false) }
}
@Test
fun handleToolbarNewPrivateTabPress() {
val browsingModeManager: BrowsingModeManager = DefaultBrowsingModeManager(BrowsingMode.Normal) {}
val item = TabCounterMenuItem.NewTab(true)
every { activity.browsingModeManager } returns browsingModeManager
controller.handleTabCounterItemInteraction(item)
assertEquals(BrowsingMode.Private, activity.browsingModeManager.mode)
verify { navController.popBackStack(R.id.homeFragment, false) }
}
}