/* 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 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.exit_tabs_screen 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.infoBanner 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.new_tab_button 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.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.state.state.TabSessionState 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.browser.InfoBanner 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.ext.updateAccessibilityCollectionInfo 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 kotlin.math.roundToInt 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, private val isInLandscape: () -> 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 val multiselectSelectionMenu: MultiselectSelectionMenu private var multiselectMenu: 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 private val checkOpenTabs = { if (isPrivateModeSelected) { view.context.components.core.store.state.privateTabs.isNotEmpty() } else { view.context.components.core.store.state.normalTabs.isNotEmpty() } } init { components.analytics.metrics.track(Event.TabsTrayOpened) toggleFabText(isPrivate) view.topBar.setOnClickListener { // no-op, consume the touch event to prevent it advancing the tray to the next state. } 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() } // We only support expanded and collapsed states. Don't allow STATE_HALF_EXPANDED. else if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) { behavior.state = BottomSheetBehavior.STATE_HIDDEN } } }) } } 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 = getTabs(isPrivate) updateBottomSheetBehavior() setTopOffset(isInLandscape()) 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) } if (hasAccessibilityEnabled) { tabsAdapter.notifyItemRangeChanged(0, tabs.size) } if (!hasLoaded) { hasLoaded = true scrollToSelectedBrowserTab() if (hasAccessibilityEnabled) { lifecycleScope.launch { delay(SELECTION_DELAY.toLong()) lifecycleScope.launch(Main) { layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex()) ?.requestFocus() layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex()) ?.sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_FOCUSED ) } } } } } } tabTrayItemMenu = TabTrayItemMenu( context = view.context, shouldShowShareAllTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 }, shouldShowSelectTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 }, hasOpenTabs = checkOpenTabs ) { when (it) { is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsOfTypeClicked( isPrivateModeSelected ) is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked() is TabTrayItemMenu.Item.SelectTabs -> interactor.onEnterMultiselect() is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( isPrivateModeSelected ) is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked() } } 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 { popupMenu -> (popupMenu.contentView as? CardView)?.setCardBackgroundColor( ContextCompat.getColor( view.context, R.color.foundation_normal_theme ) ) } } adjustNewTabButtonsForNormalMode() @Suppress("ComplexCondition") if ( view.context.settings().shouldShowGridViewBanner && view.context.settings().canShowCfr && tabs.size >= TAB_COUNT_SHOW_CFR ) { InfoBanner( context = view.context, message = view.context.getString(R.string.tab_tray_grid_view_banner_message), dismissText = view.context.getString(R.string.tab_tray_grid_view_banner_negative_button_text), actionText = view.context.getString(R.string.tab_tray_grid_view_banner_positive_button_text), container = view.infoBanner, dismissByHiding = true, dismissAction = { view.context.settings().shouldShowGridViewBanner = false } ) { interactor.onGoToTabsSettings() view.context.settings().shouldShowGridViewBanner = false }.apply { view.infoBanner.visibility = View.VISIBLE showBanner() } } else if ( view.context.settings().shouldShowAutoCloseTabsBanner && view.context.settings().canShowCfr && tabs.size >= TAB_COUNT_SHOW_CFR ) { InfoBanner( context = view.context, message = view.context.getString(R.string.tab_tray_close_tabs_banner_message), dismissText = view.context.getString(R.string.tab_tray_close_tabs_banner_negative_button_text), actionText = view.context.getString(R.string.tab_tray_close_tabs_banner_positive_button_text), container = view.infoBanner, dismissByHiding = true, dismissAction = { view.context.settings().shouldShowAutoCloseTabsBanner = false } ) { interactor.onGoToTabsSettings() view.context.settings().shouldShowAutoCloseTabsBanner = false }.apply { view.infoBanner.visibility = View.VISIBLE showBanner() } } } private fun getTabs(isPrivate: Boolean): List = if (isPrivate) { view.context.components.core.store.state.privateTabs } else { view.context.components.core.store.state.normalTabs } private fun getTabsNumberInAnyMode(): Int { return max( view.context.components.core.store.state.normalTabs.size, view.context.components.core.store.state.privateTabs.size ) } private fun getTabsNumberForExpandingTray(): Int { return if (container.context.settings().gridTabView) { EXPAND_AT_GRID_SIZE } else { EXPAND_AT_LIST_SIZE } } private fun handleTabClicked(tab: SyncTab) { interactor.onSyncedTabClicked(tab) } private fun adjustNewTabButtonsForNormalMode() { view.tab_tray_new_tab.apply { isVisible = !useFab 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() { if (enableCompactTabs) { setupCompactTabsTrayLayout() } else { setupRegularTabsTrayLayout() } // We will need to learn call setupGridTabView(), Mozilla's new // official grid layout, by preference. } private fun setupCompactTabsTrayLayout() { view.tabsTray.apply { val gridLayoutManager = GridLayoutManager(container.context, getNumberOfGridColumns(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 { getNumberOfGridColumns(container.context) } } } layoutManager = gridLayoutManager } } private fun setupRegularTabsTrayLayout() { view.tabsTray.apply { 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 } } } /** * Updates the bottom sheet height based on the number tabs or screen orientation. * Show the bottom sheet fully expanded if it is in landscape mode or the number of * tabs are greater or equal to the expand size limit. */ fun updateBottomSheetBehavior() { if (isInLandscape() || getTabsNumberInAnyMode() >= getTabsNumberForExpandingTray()) { if (useTopTabsTray) { (behavior as TopSheetBehavior).state = TopSheetBehavior.STATE_EXPANDED } else { (behavior as BottomSheetBehavior).state = BottomSheetBehavior.STATE_EXPANDED } } else { if (useTopTabsTray) { (behavior as TopSheetBehavior).state = TopSheetBehavior.STATE_COLLAPSED } else { (behavior as BottomSheetBehavior).state = BottomSheetBehavior.STATE_COLLAPSED } } } 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) scrollToSelectedBrowserTab() 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 private fun setupGridTabView() { view.tabsTray.apply { val gridLayoutManager = GridLayoutManager(container.context, getNumberOfGridColumns(container.context)) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { val numTabs = tabsAdapter.itemCount return if (position < numTabs) { 1 } else { getNumberOfGridColumns(container.context) } } } layoutManager = gridLayoutManager // Ensure items have the same all around padding - 16 dp. Avoid the double spacing issue. // A 8dp padding is already set in xml, pad the parent with the remaining needed 8dp. updateLayoutParams { val padding = GRID_ITEM_PARENT_PADDING.dpToPx(resources.displayMetrics) // Account for the already set bottom padding needed to accommodate the fab. val bottomPadding = paddingBottom + padding setPadding(padding, padding, padding, bottomPadding) } } } /** * Returns the number of columns that will fit in the grid layout for the current screen. */ private fun getNumberOfGridColumns(context: Context): Int { val displayMetrics = context.resources.displayMetrics val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density val columnCount = (screenWidthDp / COLUMN_WIDTH_DP).toInt() return if (columnCount >= 2) columnCount else 2 } private fun setupListTabView() { view.tabsTray.apply { layoutManager = LinearLayoutManager(container.context) } } 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.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, state.mode.selectedItems.size ) 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() } } } 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 } counter_text.text = updateTabCounter(browserState.normalTabs.size) updateTabTrayViewAccessibility(browserState.normalTabs.size) adjustNewTabButtonsForNormalMode() } 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( ContextCompat.getColor( view.context, if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme ) ) view.handle.updateLayoutParams { height = view.resources.getDimensionPixelSize( if (multiselect) { R.dimen.tab_tray_multiselect_handle_height } else { R.dimen.bottom_sheet_handle_height } ) if (useTopTabsTray) { bottomMargin = view.resources.getDimensionPixelSize( if (multiselect) { R.dimen.tab_tray_multiselect_handle_bottom_margin } else { R.dimen.top_sheet_handle_bottom_margin } ) } else { topMargin = view.resources.getDimensionPixelSize( if (multiselect) { R.dimen.tab_tray_multiselect_handle_top_margin } else { R.dimen.bottom_sheet_handle_top_margin } ) } } 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 updateTabTrayViewAccessibility(count: Int) { view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) { view.context?.getString(R.string.open_tab_tray_single) } else { String.format(view.context.getString(R.string.open_tab_tray_plural), count.toString()) } val isListTabView = view.context.settings().listTabView val columnCount = if (isListTabView) 1 else getNumberOfGridColumns(view.context) val rowCount = count.toDouble().div(columnCount).roundToInt() view.tabsTray.updateAccessibilityCollectionInfo(rowCount, columnCount) } 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.getDimensionPixelSize(R.dimen.tab_tray_top_offset) } 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.getString(R.string.add_private_tab) } else { fabView.new_tab_button.shrink() fabView.new_tab_button.contentDescription = view.context.getString(R.string.add_tab) } } fun onBackPressed(): Boolean { return interactor.onBackPressed() } fun scrollToSelectedBrowserTab(selectedTabId: String? = null) { view.tabsTray.apply { val selectedBrowserTabIndex = getSelectedBrowserTabViewIndex(selectedTabId) 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) smoothScrollBy( 0, -resources.getDimensionPixelSize(R.dimen.tab_tray_tab_item_height) / 2 ) } } private fun getSelectedBrowserTabViewIndex(sessionId: String? = null): Int { val tabs = if (isPrivateModeSelected) { view.context.components.core.store.state.privateTabs } else { view.context.components.core.store.state.normalTabs } return if (sessionId != null) { tabs.indexOfFirst { it.id == sessionId } } else { tabs.indexOfFirst { it.id == view.context.components.core.store.state.selectedTabId } } } companion object { private const val TAB_COUNT_SHOW_CFR = 6 private const val DEFAULT_TAB_ID = 0 private const val PRIVATE_TAB_ID = 1 // Minimum number of list items for which to show the tabs tray as expanded. private const val EXPAND_AT_LIST_SIZE = 4 // Minimum number of grid items for which to show the tabs tray as expanded. private const val EXPAND_AT_GRID_SIZE = 3 private const val SLIDE_OFFSET = 0 private const val SELECTION_DELAY = 500 private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F private const val COLUMN_WIDTH_DP = 190 // The remaining padding offset needed to provide a 16dp column spacing between the grid items. const val GRID_ITEM_PARENT_PADDING = 8 } }