/* 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 android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo import androidx.annotation.IdRes import androidx.cardview.widget.CardView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.tabs.TabLayout import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_tabs_screen_top.view.* import kotlinx.android.synthetic.main.component_tabstray_bottom.view.collect_multi_select import kotlinx.android.synthetic.main.component_tabstray_bottom.view.exit_multi_select import kotlinx.android.synthetic.main.component_tabstray_bottom.view.handle import kotlinx.android.synthetic.main.component_tabstray_bottom.view.multiselect_title import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_layout import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_empty_view import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_new_tab import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_overflow import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_wrapper import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tabsTray import kotlinx.android.synthetic.main.component_tabstray_bottom.view.topBar import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.view.* import kotlinx.android.synthetic.main.tabs_tray_tab_counter.* 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 import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.tabstray.TabViewHolder import mozilla.components.feature.syncedtabs.SyncedTabsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.toolbar.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM import org.mozilla.fenix.components.toolbar.TabCounter.Companion.MAX_VISIBLE_TABS import org.mozilla.fenix.components.toolbar.TabCounter.Companion.SO_MANY_TABS_OPEN import org.mozilla.fenix.components.topsheet.TopSheetBehavior import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode import java.text.NumberFormat import kotlin.math.max import kotlin.math.min import mozilla.components.browser.storage.sync.Tab as SyncTab /** * View that contains and configures the BrowserAwesomeBar */ @Suppress("LongParameterList", "TooManyFunctions", "LargeClass") class TabTrayView( private val container: ViewGroup, private val tabsAdapter: FenixTabsAdapter, private val interactor: TabTrayInteractor, store: TabTrayDialogFragmentStore, isPrivate: Boolean, startingInLandscape: Boolean, lifecycleOwner: LifecycleOwner, private val filterTabs: (Boolean) -> Unit ) : LayoutContainer, TabLayout.OnTabSelectedListener { val lifecycleScope = lifecycleOwner.lifecycleScope private val useFab = container.context.settings().useNewTabFloatingActionButton val fabView: View = when (container.context.settings().placeNewTabFloatingActionButtonAtTop) { true -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray_fab_top, container, true) false -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray_fab_bottom, container, true) } private val enableCompactTabs = container.context.settings().enableCompactTabs private val reverseTabOrderInTabsTray = container.context.settings().reverseTabOrderInTabsTray private val isTabsTrayFullScreenMode = container.context.settings().useFullScreenTabScreen private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled private val useTopTabsTray = container.context.settings().useTopTabsTray val view: View = if (isTabsTrayFullScreenMode) { when (useTopTabsTray) { true -> LayoutInflater.from(container.context) .inflate(R.layout.component_tabs_screen_bottom, container, true) false -> LayoutInflater.from(container.context) .inflate(R.layout.component_tabs_screen_top, container, true) } } else { when (useTopTabsTray) { true -> LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray_top, container, true) false -> LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray_bottom, container, true) } } private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID private val behavior = if (isTabsTrayFullScreenMode) null else { when (useTopTabsTray) { true -> TopSheetBehavior.from(view.tab_wrapper) false -> BottomSheetBehavior.from(view.tab_wrapper) } } private val concatAdapter = ConcatAdapter(tabsAdapter) private val tabTrayItemMenu: TabTrayItemMenu private var menu: BrowserMenu? = null private var tabsTouchHelper: TabsTouchHelper private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) private val syncedTabsController = SyncedTabsController(lifecycleOwner, view, store, concatAdapter) private val syncedTabsFeature = ViewBoundFeatureWrapper() private var hasLoaded = false override val containerView: View? get() = container private val components = container.context.components init { components.analytics.metrics.track(Event.TabsTrayOpened) toggleFabText(isPrivate) if (!isTabsTrayFullScreenMode) { if (useTopTabsTray) { (behavior as TopSheetBehavior).setTopSheetCallback(object : TopSheetBehavior.TopSheetCallback() { override fun onSlide(topSheet: View, slideOffset: Float, isOpening: Boolean?) { if (interactor.onModeRequested() is Mode.Normal && useFab) { if (slideOffset >= SLIDE_OFFSET) { fabView.new_tab_button.show() } else { fabView.new_tab_button.hide() } } } override fun onStateChanged(topSheet: View, newState: Int) { if (newState == TopSheetBehavior.STATE_HIDDEN) { components.analytics.metrics.track(Event.TabsTrayClosed) interactor.onTabTrayDismissed() } } }) } else { (behavior as BottomSheetBehavior).addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { if (interactor.onModeRequested() is Mode.Normal && useFab) { if (slideOffset >= SLIDE_OFFSET) { fabView.new_tab_button.show() } else { fabView.new_tab_button.hide() } } } override fun onStateChanged(bottomSheet: View, newState: Int) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { components.analytics.metrics.track(Event.TabsTrayClosed) interactor.onTabTrayDismissed() } } }) } } if (isTabsTrayFullScreenMode) { view.exit_tabs_screen.setOnClickListener { interactor.onTabTrayDismissed() } } val selectedTabIndex = if (!isPrivate) { DEFAULT_TAB_ID } else { PRIVATE_TAB_ID } view.tab_layout.getTabAt(selectedTabIndex)?.also { view.tab_layout.selectTab(it, true) } view.tab_layout.addOnTabSelectedListener(this) val tabs = if (isPrivate) { view.context.components.core.store.state.privateTabs } else { view.context.components.core.store.state.normalTabs } val selectedBrowserTabIndex = tabs .indexOfFirst { it.id == view.context.components.core.store.state.selectedTabId } if (tabs.size > EXPAND_AT_SIZE || startingInLandscape) { expand() } setTopOffset(startingInLandscape) if (view.context.settings().syncedTabsInTabsTray) { syncedTabsFeature.set( feature = SyncedTabsFeature( context = container.context, storage = components.backgroundServices.syncedTabsStorage, accountManager = components.backgroundServices.accountManager, view = syncedTabsController, lifecycleOwner = lifecycleOwner, onTabClicked = ::handleTabClicked ), owner = lifecycleOwner, view = view ) } updateTabsTrayLayout() view.tabsTray.apply { adapter = concatAdapter tabsTouchHelper = TabsTouchHelper( observable = tabsAdapter, onViewHolderTouched = { it is TabViewHolder } ) tabsTouchHelper.attachToRecyclerView(this) tabsAdapter.tabTrayInteractor = interactor tabsAdapter.onTabsUpdated = { // Put the 'Add to collections' button after the tabs have loaded. // And, put the Synced Tabs adapter at the end. if (reverseTabOrderInTabsTray) { // Put these at the start when reverse tab order is enabled. Also, we disallow // reverse tab order for compact tabs in settings. concatAdapter.addAdapter(0, collectionsButtonAdapter) concatAdapter.addAdapter(0, syncedTabsController.adapter) } else { concatAdapter.addAdapter(collectionsButtonAdapter) concatAdapter.addAdapter(syncedTabsController.adapter) } // Disabling the following block of code because it causes a crash when // accessibility services are enabled! `notifyDataSetChanged()` is incompatible // with concatAdapter. See: https://github.com/mozilla-mobile/fenix/issues/14540 // WARNING: Merging the upstream fix for this will cause lot of conflicts! // // if (hasAccessibilityEnabled) { // tabsAdapter.notifyItemRangeChanged(0, tabs.size) // } if (!hasLoaded) { hasLoaded = true scrollToTab(view.context.components.core.store.state.selectedTabId) if (hasAccessibilityEnabled) { lifecycleScope.launch { delay(SELECTION_DELAY.toLong()) lifecycleScope.launch(Main) { layoutManager?.findViewByPosition(selectedBrowserTabIndex) ?.requestFocus() layoutManager?.findViewByPosition(selectedBrowserTabIndex) ?.sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_FOCUSED ) } } } } } } tabTrayItemMenu = TabTrayItemMenu(view.context, { view.tab_layout.selectedTabPosition == 0 }) { when (it) { is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked( isPrivateModeSelected ) is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked() is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect() is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( isPrivateModeSelected ) is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked() } } 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 ) ) } } adjustNewTabButtonForNormalMode() } private fun gridViewNumberOfCols(context: Context): Int { val displayMetrics = context.resources.displayMetrics val dpWidth = displayMetrics.widthPixels / displayMetrics.density val columnWidthDp = 190 val columnCount = (dpWidth / columnWidthDp).toInt() return if (columnCount >= 2) columnCount else 2 } private fun handleTabClicked(tab: SyncTab) { interactor.onSyncedTabClicked(tab) } private fun adjustNewTabButtonForNormalMode() { view.tab_tray_new_tab.apply { visibility = if (useFab) View.GONE else View.VISIBLE setOnClickListener { sendNewTabEvent(isPrivateModeSelected) interactor.onNewTabTapped(isPrivateModeSelected) } } fabView.new_tab_button.apply { isVisible = useFab setOnClickListener { sendNewTabEvent(isPrivateModeSelected) interactor.onNewTabTapped(isPrivateModeSelected) } } } private fun sendNewTabEvent(isPrivateModeSelected: Boolean) { val eventToSend = if (isPrivateModeSelected) { Event.NewPrivateTabTapped } else { Event.NewTabTapped } components.analytics.metrics.track(eventToSend) } fun updateTabsTrayLayout() { view.tabsTray.apply { if (enableCompactTabs) { val gridLayoutManager = GridLayoutManager(container.context, gridViewNumberOfCols(container.context)) if (useTopTabsTray) { gridLayoutManager.reverseLayout = true } gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { val numTabs = tabsAdapter.itemCount return if (position < numTabs) { 1 } else { gridViewNumberOfCols(container.context) } } } layoutManager = gridLayoutManager } else { val linearLayoutManager = LinearLayoutManager(container.context) if (useTopTabsTray) { if (!reverseTabOrderInTabsTray) { linearLayoutManager.reverseLayout = true } else { linearLayoutManager.stackFromEnd = true } } else { if (reverseTabOrderInTabsTray) { linearLayoutManager.reverseLayout = true linearLayoutManager.stackFromEnd = true } } layoutManager = linearLayoutManager } } } fun expand() { if (!isTabsTrayFullScreenMode) { if (useTopTabsTray) { (behavior as TopSheetBehavior).state = TopSheetBehavior.STATE_EXPANDED } else { (behavior as BottomSheetBehavior).state = BottomSheetBehavior.STATE_EXPANDED } } } enum class TabChange { PRIVATE, NORMAL } private fun toggleSaveToCollectionButton(isPrivate: Boolean) { collectionsButtonAdapter.notifyItemChanged( 0, if (isPrivate) TabChange.PRIVATE else TabChange.NORMAL ) } override fun onTabSelected(tab: TabLayout.Tab?) { toggleFabText(isPrivateModeSelected) filterTabs.invoke(isPrivateModeSelected) toggleSaveToCollectionButton(isPrivateModeSelected) updateUINormalMode(view.context.components.core.store.state) scrollToTab(view.context.components.core.store.state.selectedTabId) if (isPrivateModeSelected) { components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped) } else { components.analytics.metrics.track(Event.TabsTrayNormalModeTapped) } } override fun onTabReselected(tab: TabLayout.Tab?) = Unit override fun onTabUnselected(tab: TabLayout.Tab?) = Unit var mode: Mode = Mode.Normal private set fun updateState(state: TabTrayDialogFragmentState) { val oldMode = mode if (oldMode::class != state.mode::class) { updateTabsForMultiselectModeChanged(state.mode is Mode.MultiSelect) if (hasAccessibilityEnabled) { view.announceForAccessibility( if (state.mode == Mode.Normal) view.context.getString( R.string.tab_tray_exit_multiselect_content_description ) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description) ) } } mode = state.mode when (state.mode) { Mode.Normal -> { view.tabsTray.apply { tabsTouchHelper.attachToRecyclerView(this) } toggleUIMultiselect(multiselect = false) updateUINormalMode(state.browserState) } is Mode.MultiSelect -> { // Disable swipe to delete while in multiselect tabsTouchHelper.attachToRecyclerView(null) toggleUIMultiselect(multiselect = true) fabView.new_tab_button.isVisible = false view.tab_tray_new_tab.isVisible = false view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty() view.multiselect_title.text = view.context.getString( R.string.tab_tray_multi_select_title, state.mode.selectedItems.size ) view.collect_multi_select.setOnClickListener { interactor.onSaveToCollectionClicked(state.mode.selectedItems) } view.exit_multi_select.setOnClickListener { interactor.onBackPressed() } } } if (oldMode.selectedItems != state.mode.selectedItems) { val unselectedItems = oldMode.selectedItems - state.mode.selectedItems state.mode.selectedItems.union(unselectedItems).forEach { item -> if (hasAccessibilityEnabled) { view.announceForAccessibility( if (unselectedItems.contains(item)) view.context.getString( R.string.tab_tray_item_unselected_multiselect_content_description, item.title ) else view.context.getString( R.string.tab_tray_item_selected_multiselect_content_description, item.title ) ) } updateTabsForSelectionChanged(item.id) } } } private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) { this.findViewById(childId)?.let { val constraintSet = ConstraintSet() constraintSet.clone(this) constraintSet.constrainPercentWidth(it.id, percentage) constraintSet.applyTo(this) it.requestLayout() } } private fun updateUINormalMode(browserState: BrowserState) { val hasNoTabs = if (isPrivateModeSelected) { browserState.privateTabs.isEmpty() } else { browserState.normalTabs.isEmpty() } view.tab_tray_empty_view.isVisible = hasNoTabs if (hasNoTabs) { view.tab_tray_empty_view.text = if (isPrivateModeSelected) { view.context.getString(R.string.no_private_tabs_description) } else { view.context?.getString(R.string.no_open_tabs_description) } } view.tabsTray.visibility = if (hasNoTabs) { View.INVISIBLE } else { View.VISIBLE } view.tab_tray_overflow.isVisible = !hasNoTabs counter_text.text = updateTabCounter(browserState.normalTabs.size) updateTabCounterContentDescription(browserState.normalTabs.size) adjustNewTabButtonForNormalMode() } private fun toggleUIMultiselect(multiselect: Boolean) { view.multiselect_title.isVisible = multiselect view.collect_multi_select.isVisible = multiselect view.exit_multi_select.isVisible = multiselect view.topBar.setBackgroundColor( ContextCompat.getColor( view.context, if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme ) ) val displayMetrics = view.context.resources.displayMetrics view.handle.updateLayoutParams { height = if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx( displayMetrics ) if (useTopTabsTray) { bottomMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_BOTTOM_MARGIN.dpToPx( displayMetrics ) } else { topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx( displayMetrics ) } } view.tab_wrapper.setChildWPercent( if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH, view.handle.id ) view.handle.setBackgroundColor( ContextCompat.getColor( view.context, if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme ) ) if (isTabsTrayFullScreenMode) { view.exit_tabs_screen.isVisible = !multiselect } view.tab_layout.isVisible = !multiselect view.tab_tray_empty_view.isVisible = !multiselect view.tab_tray_overflow.isVisible = !multiselect } private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) { view.tabsTray.apply { val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs( isPrivateModeSelected ) collectionsButtonAdapter.notifyItemChanged( 0, if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL ) tabsAdapter.notifyItemRangeChanged(0, tabs.size, true) } } private fun updateTabsForSelectionChanged(itemId: String) { view.tabsTray.apply { val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs( isPrivateModeSelected ) val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId } tabsAdapter.notifyItemChanged( selectedBrowserTabIndex, true ) } } private fun updateTabCounterContentDescription(count: Int) { view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) { view.context?.getString(R.string.open_tab_tray_single) } else { view.context?.getString(R.string.open_tab_tray_plural, count.toString()) } view.tabsTray.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View?, info: AccessibilityNodeInfo? ) { super.onInitializeAccessibilityNodeInfo(host, info) info?.let { info.collectionInfo = CollectionInfo.obtain( tabsAdapter.tabCount, 1, false ) } } } } private fun updateTabCounter(count: Int): String { if (count > MAX_VISIBLE_TABS) { counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM) return SO_MANY_TABS_OPEN } return NumberFormat.getInstance().format(count.toLong()) } fun setTopOffset(landscape: Boolean) { if (!isTabsTrayFullScreenMode) { val topOffset = if (landscape) { 0 } else { view.context.resources.getDimension(R.dimen.tab_tray_top_offset).toInt() } if (!useTopTabsTray) { (behavior as BottomSheetBehavior).setExpandedOffset(topOffset) } } } fun dismissMenu() { menu?.dismiss() } private fun toggleFabText(private: Boolean) { if (private) { fabView.new_tab_button.extend() fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_private_tab) } else { fabView.new_tab_button.shrink() fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_tab) } } fun onBackPressed(): Boolean { return interactor.onBackPressed() } fun scrollToTab(sessionId: String?) { view.tabsTray.apply { val tabs = if (isPrivateModeSelected) { view.context.components.core.store.state.privateTabs } else { view.context.components.core.store.state.normalTabs } val selectedBrowserTabIndex = tabs .indexOfFirst { it.id == sessionId } val recyclerViewIndex = if (reverseTabOrderInTabsTray) { // For reverse tab order and non-compact tabs, we add the items in collections button // adapter and synced tabs adapter, and offset by 1 to show the tab above the // current tab, unless current tab is first in reverse order. min(selectedBrowserTabIndex + 1, tabsAdapter.itemCount - 1) + collectionsButtonAdapter.itemCount + syncedTabsController.adapter.itemCount } else { // We offset index by -1 to show the tab above the current tab, unless current tab // is the first. max(0, selectedBrowserTabIndex - 1) } layoutManager?.scrollToPosition(recyclerViewIndex) } } companion object { private const val DEFAULT_TAB_ID = 0 private const val PRIVATE_TAB_ID = 1 private const val EXPAND_AT_SIZE = 3 private const val SLIDE_OFFSET = 0 private const val SELECTION_DELAY = 500 private const val MULTISELECT_HANDLE_HEIGHT = 11 private const val NORMAL_HANDLE_HEIGHT = 3 private const val NORMAL_TOP_MARGIN = 8 private const val NORMAL_BOTTOM_MARGIN = 8 private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F } } class TabTrayItemMenu( private val context: Context, private val shouldShowSaveToCollection: () -> 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) }, 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) } ) } }