For #14376: Use concept-menu with tab counter menu (#14374)

pull/35/head
Tiger Oakes 4 years ago committed by GitHub
parent a8291bf641
commit e93a8b7d34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,7 @@ open class BrowserInteractor(
browserToolbarController.handleTabCounterClick()
}
override fun onTabCounterMenuItemTapped(item: TabCounterMenuItem) {
override fun onTabCounterMenuItemTapped(item: TabCounterMenu.Item) {
browserToolbarController.handleTabCounterItemInteraction(item)
}

@ -32,7 +32,7 @@ interface BrowserToolbarController {
fun handleToolbarPasteAndGo(text: String)
fun handleToolbarClick()
fun handleTabCounterClick()
fun handleTabCounterItemInteraction(item: TabCounterMenuItem)
fun handleTabCounterItemInteraction(item: TabCounterMenu.Item)
fun handleReaderModePressed(enabled: Boolean)
}
@ -129,9 +129,9 @@ class DefaultBrowserToolbarController(
}
}
override fun handleTabCounterItemInteraction(item: TabCounterMenuItem) {
override fun handleTabCounterItemInteraction(item: TabCounterMenu.Item) {
when (item) {
is TabCounterMenuItem.CloseTab -> {
is TabCounterMenu.Item.CloseTab -> {
sessionManager.selectedSession?.let {
// When closing the last tab we must show the undo snackbar in the home fragment
if (sessionManager.sessionsOfType(it.private).count() == 1) {
@ -148,8 +148,8 @@ class DefaultBrowserToolbarController(
}
}
}
is TabCounterMenuItem.NewTab -> {
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(item.isPrivate)
is TabCounterMenu.Item.NewTab -> {
activity.browsingModeManager.mode = item.mode
navController.popBackStack(R.id.homeFragment, false)
}
}

@ -44,7 +44,7 @@ interface BrowserToolbarViewInteractor {
fun onBrowserToolbarClicked()
fun onBrowserToolbarMenuItemTapped(item: ToolbarMenu.Item)
fun onTabCounterClicked()
fun onTabCounterMenuItemTapped(item: TabCounterMenuItem)
fun onTabCounterMenuItemTapped(item: TabCounterMenu.Item)
fun onScrolled(offset: Int)
fun onReaderModePressed(enabled: Boolean)
}

@ -0,0 +1,100 @@
/* 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.components.toolbar
import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.MenuCandidate
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
class TabCounterMenu(
context: Context,
private val metrics: MetricController,
private val onItemTapped: (Item) -> Unit
) {
sealed class Item {
object CloseTab : Item()
data class NewTab(val mode: BrowsingMode) : Item()
}
val menuController: MenuController by lazy { BrowserMenuController() }
private val newTabItem: TextMenuCandidate
private val newPrivateTabItem: TextMenuCandidate
private val closeTabItem: TextMenuCandidate
init {
val primaryTextColor = context.getColorFromAttr(R.attr.primaryText)
val textStyle = TextStyle(color = primaryTextColor)
newTabItem = TextMenuCandidate(
text = context.getString(R.string.browser_menu_new_tab),
start = DrawableMenuIcon(
context,
R.drawable.ic_new,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
onItemTapped(Item.NewTab(BrowsingMode.Normal))
}
newPrivateTabItem = TextMenuCandidate(
text = context.getString(R.string.home_screen_shortcut_open_new_private_tab_2),
start = DrawableMenuIcon(
context,
R.drawable.ic_private_browsing,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
onItemTapped(Item.NewTab(BrowsingMode.Private))
}
closeTabItem = TextMenuCandidate(
text = context.getString(R.string.close_tab),
start = DrawableMenuIcon(
context,
R.drawable.ic_close,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB))
onItemTapped(Item.CloseTab)
}
}
@VisibleForTesting
internal fun menuItems(showOnly: BrowsingMode?): List<MenuCandidate> {
return when (showOnly) {
BrowsingMode.Normal -> listOf(newTabItem)
BrowsingMode.Private -> listOf(newPrivateTabItem)
null -> listOf(
newTabItem,
newPrivateTabItem,
DividerMenuCandidate(),
closeTabItem
)
}
}
fun updateMenu(showOnly: BrowsingMode? = null) {
menuController.submitList(menuItems(showOnly))
}
}

@ -1,10 +0,0 @@
/* 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.components.toolbar
sealed class TabCounterMenuItem {
object CloseTab : TabCounterMenuItem()
class NewTab(val isPrivate: Boolean) : TabCounterMenuItem()
}

@ -4,27 +4,18 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
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.state.selector.getNormalOrPrivateTabs
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
/**
@ -34,18 +25,25 @@ import java.lang.ref.WeakReference
class TabCounterToolbarButton(
private val lifecycleOwner: LifecycleOwner,
private val isPrivate: Boolean,
private val onItemTapped: (TabCounterMenuItem) -> Unit = {},
private val onItemTapped: (TabCounterMenu.Item) -> Unit = {},
private val showTabs: () -> Unit
) : Toolbar.Action {
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
override fun createView(parent: ViewGroup): View {
parent.context.components.core.store.flowScoped(lifecycleOwner) { flow ->
val store = parent.context.components.core.store
val metrics = parent.context.components.analytics.metrics
store.flowScoped(lifecycleOwner) { flow ->
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate).size }
.ifChanged()
.collect { tabs -> updateCount(tabs) }
}
val menu = TabCounterMenu(parent.context, metrics, onItemTapped)
menu.updateMenu()
val view = TabCounter(parent.context).apply {
reference = WeakReference(this)
setOnClickListener {
@ -53,28 +51,23 @@ class TabCounterToolbarButton(
}
setOnLongClickListener {
getTabContextMenu(it.context).show(it)
menu.menuController.show(anchor = it)
true
}
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setCount(context.components.core.store.state.getNormalOrPrivateTabs(isPrivate).size)
setCount(store.state.getNormalOrPrivateTabs(isPrivate).size)
}
override fun onViewDetachedFromWindow(v: View?) { /* no-op */
}
override fun onViewDetachedFromWindow(v: View?) { /* no-op */ }
})
}
// Set selectableItemBackgroundBorderless
val outValue = TypedValue()
parent.context.theme.resolveAttribute(
android.R.attr.selectableItemBackgroundBorderless,
outValue,
true
)
view.setBackgroundResource(outValue.resourceId)
view.setBackgroundResource(parent.context.theme.resolveAttribute(
android.R.attr.selectableItemBackgroundBorderless
))
return view
}
@ -83,46 +76,4 @@ class TabCounterToolbarButton(
private fun updateCount(count: Int) {
reference.get()?.setCountWithAnimation(count)
}
private fun getTabContextMenu(context: Context): BrowserMenu {
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
val metrics = context.components.analytics.metrics
val menuItems = listOf(
BrowserMenuImageText(
label = context.getString(R.string.browser_menu_new_tab),
imageResource = R.drawable.ic_new,
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
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
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
onItemTapped(TabCounterMenuItem.NewTab(true))
},
BrowserMenuDivider(),
BrowserMenuImageText(
label = context.getString(R.string.close_tab),
imageResource = R.drawable.ic_close,
iconTintColorResource = primaryTextColor,
textColorResource = primaryTextColor
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB))
onItemTapped(TabCounterMenuItem.CloseTab)
}
)
return BrowserMenuBuilder(
when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> menuItems.reversed()
ToolbarPosition.TOP -> menuItems
}
).build(context)
}
}

@ -132,14 +132,17 @@ class DefaultToolbarIntegration(
)
}
val onTabCounterMenuItemTapped = { item: TabCounterMenuItem ->
interactor.onTabCounterMenuItemTapped(item)
}
val tabsAction =
TabCounterToolbarButton(lifecycleOwner, isPrivate, onTabCounterMenuItemTapped) {
val tabsAction = TabCounterToolbarButton(
lifecycleOwner,
isPrivate,
onItemTapped = {
interactor.onTabCounterMenuItemTapped(it)
},
showTabs = {
toolbar.hideKeyboard()
interactor.onTabCounterClicked()
}
)
toolbar.addBrowserAction(tabsAction)
val engineForSpeculativeConnects = if (!isPrivate) engine else null

@ -52,9 +52,6 @@ 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
@ -86,6 +83,7 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.FenixTipManager
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
import org.mozilla.fenix.components.toolbar.TabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
@ -311,7 +309,7 @@ class HomeFragment : Fragment() {
}
}
@SuppressWarnings("LongMethod")
@Suppress("LongMethod", "ComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -348,8 +346,21 @@ class HomeFragment : Fragment() {
}
createHomeMenu(requireContext(), WeakReference(view.menuButton))
val tabCounterMenu = TabCounterMenu(
view.context,
metrics = view.context.components.analytics.metrics
) {
if (it is TabCounterMenu.Item.NewTab) {
(activity as HomeActivity).browsingModeManager.mode = it.mode
}
}
val inverseBrowsingMode = when ((activity as HomeActivity).browsingModeManager.mode) {
BrowsingMode.Normal -> BrowsingMode.Private
BrowsingMode.Private -> BrowsingMode.Normal
}
tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
view.tab_button.setOnLongClickListener {
createTabCounterMenu(requireContext()).show(view.tab_button)
tabCounterMenu.menuController.show(anchor = it)
true
}
@ -710,50 +721,6 @@ class HomeFragment : Fragment() {
nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
}
private fun openInNormalTab(url: String) {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = true,
from = BrowserDirection.FromHome
)
}
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
) {
requireComponents.analytics.metrics.track(
Event.TabCounterMenuItemTapped(
if (isPrivate) {
Event.TabCounterMenuItemTapped.Item.NEW_TAB
} else {
Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB
}
)
)
(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(

@ -30,7 +30,7 @@ class BrowserInteractorTest {
@Test
fun onTabCounterMenuItemTapped() {
val item: TabCounterMenuItem = mockk()
val item: TabCounterMenu.Item = mockk()
interactor.onTabCounterMenuItemTapped(item)
verify { browserToolbarController.handleTabCounterItemInteraction(item) }

@ -180,7 +180,7 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleToolbarCloseTabPressWithLastPrivateSession() {
val browsingModeManager = SimpleBrowsingModeManager(BrowsingMode.Private)
val item = TabCounterMenuItem.CloseTab
val item = TabCounterMenu.Item.CloseTab
val sessions = listOf(
mockk<Session> {
every { private } returns true
@ -201,7 +201,7 @@ class DefaultBrowserToolbarControllerTest {
fun handleToolbarCloseTabPress() {
val tabsUseCases: TabsUseCases = mockk(relaxed = true)
val removeTabUseCase: TabsUseCases.RemoveTabUseCase = mockk(relaxed = true)
val item = TabCounterMenuItem.CloseTab
val item = TabCounterMenu.Item.CloseTab
every { sessionManager.sessions } returns emptyList()
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
@ -215,7 +215,7 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleToolbarNewTabPress() {
val browsingModeManager = SimpleBrowsingModeManager(BrowsingMode.Private)
val item = TabCounterMenuItem.NewTab(false)
val item = TabCounterMenu.Item.NewTab(BrowsingMode.Normal)
every { activity.browsingModeManager } returns browsingModeManager
every { navController.popBackStack(R.id.homeFragment, any()) } returns true
@ -229,7 +229,7 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleToolbarNewPrivateTabPress() {
val browsingModeManager = SimpleBrowsingModeManager(BrowsingMode.Normal)
val item = TabCounterMenuItem.NewTab(true)
val item = TabCounterMenu.Item.NewTab(BrowsingMode.Private)
every { activity.browsingModeManager } returns browsingModeManager
every { navController.popBackStack(R.id.homeFragment, any()) } returns true

@ -0,0 +1,95 @@
/* 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.components.toolbar
import android.content.Context
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verifyAll
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class TabCounterMenuTest {
private lateinit var context: Context
private lateinit var metrics: MetricController
private lateinit var onItemTapped: (TabCounterMenu.Item) -> Unit
private lateinit var menu: TabCounterMenu
@Before
fun setup() {
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
metrics = mockk(relaxed = true)
onItemTapped = mockk(relaxed = true)
menu = TabCounterMenu(context, metrics, onItemTapped)
}
@Test
fun `all items use primary text color styling`() {
val items = menu.menuItems(showOnly = null)
assertEquals(4, items.size)
val textItems = items.mapNotNull { it as? TextMenuCandidate }
assertEquals(3, textItems.size)
val primaryTextColor = context.getColor(R.color.primary_text_normal_theme)
for (item in textItems) {
assertEquals(primaryTextColor, item.textStyle.color)
assertEquals(primaryTextColor, (item.start as DrawableMenuIcon).tint)
}
}
@Test
fun `return only the new tab item`() {
val items = menu.menuItems(showOnly = BrowsingMode.Normal)
assertEquals(1, items.size)
val item = items[0] as TextMenuCandidate
assertEquals("New tab", item.text)
item.onClick()
verifyAll {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
onItemTapped(TabCounterMenu.Item.NewTab(BrowsingMode.Normal))
}
}
@Test
fun `return only the new private tab item`() {
val items = menu.menuItems(showOnly = BrowsingMode.Private)
assertEquals(1, items.size)
val item = items[0] as TextMenuCandidate
assertEquals("New private tab", item.text)
item.onClick()
verifyAll {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
onItemTapped(TabCounterMenu.Item.NewTab(BrowsingMode.Private))
}
}
@Test
fun `return two new tab items and a close button`() {
val (newTab, newPrivateTab, divider, closeTab) = menu.menuItems(showOnly = null)
assertEquals("New tab", (newTab as TextMenuCandidate).text)
assertEquals("New private tab", (newPrivateTab as TextMenuCandidate).text)
assertEquals("Close tab", (closeTab as TextMenuCandidate).text)
assertEquals(DividerMenuCandidate(), divider)
}
}
Loading…
Cancel
Save