mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
Bug 1923855 - Move tab counter to tab strip r=android-reviewers,007
Differential Revision: https://phabricator.services.mozilla.com/D225654
This commit is contained in:
parent
18295d9284
commit
aa1fa29243
@ -508,16 +508,7 @@ abstract class BaseBrowserFragment :
|
||||
customTabSessionId = customTabSessionId,
|
||||
browserAnimator = browserAnimator,
|
||||
onTabCounterClicked = {
|
||||
thumbnailsFeature.get()?.requestScreenshot()
|
||||
findNavController().nav(
|
||||
R.id.browserFragment,
|
||||
BrowserFragmentDirections.actionGlobalTabsTrayFragment(
|
||||
page = when (activity.browsingModeManager.mode) {
|
||||
BrowsingMode.Normal -> Page.NormalTabs
|
||||
BrowsingMode.Private -> Page.PrivateTabs
|
||||
},
|
||||
),
|
||||
)
|
||||
onTabCounterClicked(activity.browsingModeManager.mode)
|
||||
},
|
||||
onCloseTab = { closedSession ->
|
||||
val closedTab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController
|
||||
@ -593,6 +584,7 @@ abstract class BaseBrowserFragment :
|
||||
BrowserFragmentDirections.actionGlobalHome(),
|
||||
)
|
||||
},
|
||||
onTabCounterClick = { onTabCounterClicked(activity.browsingModeManager.mode) },
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -1688,16 +1680,7 @@ abstract class BaseBrowserFragment :
|
||||
},
|
||||
onTabsButtonClick = {
|
||||
NavigationBar.browserTabTrayTapped.record(NoExtras())
|
||||
thumbnailsFeature.get()?.requestScreenshot()
|
||||
findNavController().nav(
|
||||
R.id.browserFragment,
|
||||
BrowserFragmentDirections.actionGlobalTabsTrayFragment(
|
||||
page = when (activity.browsingModeManager.mode) {
|
||||
BrowsingMode.Normal -> Page.NormalTabs
|
||||
BrowsingMode.Private -> Page.PrivateTabs
|
||||
},
|
||||
),
|
||||
)
|
||||
onTabCounterClicked(activity.browsingModeManager.mode)
|
||||
},
|
||||
onTabsButtonLongPress = {
|
||||
NavigationBar.browserTabTrayLongTapped.record(NoExtras())
|
||||
@ -1717,6 +1700,19 @@ abstract class BaseBrowserFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTabCounterClicked(browsingMode: BrowsingMode) {
|
||||
thumbnailsFeature.get()?.requestScreenshot()
|
||||
findNavController().nav(
|
||||
R.id.browserFragment,
|
||||
BrowserFragmentDirections.actionGlobalTabsTrayFragment(
|
||||
page = when (browsingMode) {
|
||||
BrowsingMode.Normal -> Page.NormalTabs
|
||||
BrowsingMode.Private -> Page.PrivateTabs
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun initializeMicrosurveyFeature(context: Context) {
|
||||
if (context.settings().isExperimentationEnabled && context.settings().microsurveyFeatureEnabled) {
|
||||
|
@ -0,0 +1,88 @@
|
||||
/* 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.browser.tabstrip
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.menu.MenuItem
|
||||
import org.mozilla.fenix.compose.text.Text
|
||||
|
||||
/**
|
||||
* Model representing different tab strip tab counter menu items.
|
||||
*/
|
||||
sealed interface TabCounterMenuItem {
|
||||
|
||||
/**
|
||||
* Model representing menu items with an icon.
|
||||
*
|
||||
* @property textResource The text resource to be displayed.
|
||||
* @property drawableRes The drawable resource to be displayed.
|
||||
* @property onClick Invoked when the item is clicked.
|
||||
*/
|
||||
sealed class IconItem(
|
||||
@StringRes val textResource: Int,
|
||||
@DrawableRes val drawableRes: Int,
|
||||
open val onClick: () -> Unit,
|
||||
) : TabCounterMenuItem {
|
||||
|
||||
/**
|
||||
* Model representing a new tab menu item.
|
||||
*
|
||||
* @property onClick Invoked when the item is clicked.
|
||||
*/
|
||||
data class NewTab(
|
||||
override val onClick: () -> Unit,
|
||||
) : IconItem(
|
||||
textResource = R.string.add_tab,
|
||||
drawableRes = R.drawable.mozac_ic_plus_24,
|
||||
onClick = onClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Model representing a new private tab menu item.
|
||||
*
|
||||
* @property onClick Invoked when the item is clicked.
|
||||
*/
|
||||
data class NewPrivateTab(
|
||||
override val onClick: () -> Unit,
|
||||
) : IconItem(
|
||||
textResource = R.string.add_private_tab,
|
||||
drawableRes = R.drawable.mozac_ic_private_mode_24,
|
||||
onClick = onClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Model representing a close tab menu item.
|
||||
*
|
||||
* @property onClick Invoked when the item is clicked.
|
||||
*/
|
||||
data class CloseTab(
|
||||
override val onClick: () -> Unit,
|
||||
) : IconItem(
|
||||
textResource = R.string.close_tab,
|
||||
drawableRes = R.drawable.mozac_ic_cross_24,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Model representing a divider.
|
||||
*/
|
||||
data object Divider : TabCounterMenuItem
|
||||
|
||||
/**
|
||||
* Maps [TabCounterMenuItem] to a [MenuItem].
|
||||
*/
|
||||
fun toMenuItem(): MenuItem =
|
||||
when (this) {
|
||||
is Divider -> MenuItem.Divider
|
||||
is IconItem -> MenuItem.IconItem(
|
||||
text = Text.Resource(textResource),
|
||||
drawableRes = drawableRes,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
@ -86,8 +86,9 @@ private val maxTabStripItemWidth = 280.dp
|
||||
private val tabItemHeight = 40.dp
|
||||
private val tabStripIconSize = 24.dp
|
||||
private val spaceBetweenTabs = 4.dp
|
||||
private val tabStripStartPadding = 8.dp
|
||||
private val tabStripListContentStartPadding = 8.dp
|
||||
private val titleFadeWidth = 16.dp
|
||||
private val tabStripHorizontalPadding = 16.dp
|
||||
|
||||
/**
|
||||
* Top level composable for the tabs strip.
|
||||
@ -101,6 +102,7 @@ private val titleFadeWidth = 16.dp
|
||||
* @param onLastTabClose Invoked when the last remaining open tab is closed.
|
||||
* @param onSelectedTabClick Invoked when a tab is selected.
|
||||
* @param onPrivateModeToggleClick Invoked when the private mode toggle button is clicked.
|
||||
* @param onTabCounterClick Invoked when tab counter is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun TabStrip(
|
||||
@ -113,26 +115,47 @@ fun TabStrip(
|
||||
onLastTabClose: (isPrivate: Boolean) -> Unit,
|
||||
onSelectedTabClick: () -> Unit,
|
||||
onPrivateModeToggleClick: (mode: BrowsingMode) -> Unit,
|
||||
onTabCounterClick: () -> Unit,
|
||||
) {
|
||||
val isPossiblyPrivateMode by appStore.observeAsState(false) { it.mode.isPrivate }
|
||||
val state by browserStore.observeAsState(TabStripState.initial) {
|
||||
it.toTabStripState(isSelectDisabled = onHome, isPossiblyPrivateMode = isPossiblyPrivateMode)
|
||||
it.toTabStripState(
|
||||
isSelectDisabled = onHome,
|
||||
isPossiblyPrivateMode = isPossiblyPrivateMode,
|
||||
addTab = onAddTabClick,
|
||||
toggleBrowsingMode = { isPrivate ->
|
||||
toggleBrowsingMode(isPrivate, onPrivateModeToggleClick, appStore)
|
||||
},
|
||||
closeTab = { isPrivate, numberOfTabs ->
|
||||
it.selectedTabId?.let { selectedTabId ->
|
||||
closeTab(
|
||||
numberOfTabs = numberOfTabs,
|
||||
isPrivate = isPrivate,
|
||||
tabsUseCases = tabsUseCases,
|
||||
tabId = selectedTabId,
|
||||
onLastTabClose = onLastTabClose,
|
||||
onCloseTabClick = onCloseTabClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
TabStripContent(
|
||||
state = state,
|
||||
onAddTabClick = onAddTabClick,
|
||||
onPrivateModeToggleClick = {
|
||||
val newMode = BrowsingMode.fromBoolean(!state.isPrivateMode)
|
||||
onPrivateModeToggleClick(newMode)
|
||||
appStore.dispatch(AppAction.ModeChange(newMode))
|
||||
toggleBrowsingMode(state.isPrivateMode, onPrivateModeToggleClick, appStore)
|
||||
},
|
||||
onCloseTabClick = { id, isPrivate ->
|
||||
if (state.tabs.size == 1) {
|
||||
onLastTabClose(isPrivate)
|
||||
}
|
||||
tabsUseCases.removeTab(id)
|
||||
onCloseTabClick(isPrivate)
|
||||
onCloseTabClick = { tabId, isPrivate ->
|
||||
closeTab(
|
||||
numberOfTabs = state.tabs.size,
|
||||
isPrivate = isPrivate,
|
||||
tabsUseCases = tabsUseCases,
|
||||
tabId = tabId,
|
||||
onLastTabClose = onLastTabClose,
|
||||
onCloseTabClick = onCloseTabClick,
|
||||
)
|
||||
},
|
||||
onSelectedTabClick = {
|
||||
tabsUseCases.selectTab(it)
|
||||
@ -143,6 +166,7 @@ fun TabStrip(
|
||||
tabsUseCases.moveTabs(listOf(tabId), targetId, placeAfter)
|
||||
}
|
||||
},
|
||||
onTabCounterClick = onTabCounterClick,
|
||||
)
|
||||
}
|
||||
|
||||
@ -154,44 +178,58 @@ private fun TabStripContent(
|
||||
onCloseTabClick: (id: String, isPrivate: Boolean) -> Unit,
|
||||
onSelectedTabClick: (id: String) -> Unit,
|
||||
onMove: (tabId: String, targetId: String, placeAfter: Boolean) -> Unit,
|
||||
onTabCounterClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FirefoxTheme.colors.layer3)
|
||||
.systemGestureExclusion(),
|
||||
.systemGestureExclusion()
|
||||
.padding(horizontal = tabStripHorizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onPrivateModeToggleClick,
|
||||
modifier = Modifier.padding(start = tabStripStartPadding),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.mozac_ic_private_mode_24),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
contentDescription = if (state.isPrivateMode) {
|
||||
stringResource(R.string.content_description_disable_private_browsing_button)
|
||||
} else {
|
||||
stringResource(R.string.content_description_private_browsing_button)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
TabsList(
|
||||
state = state,
|
||||
Row(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
onCloseTabClick = onCloseTabClick,
|
||||
onSelectedTabClick = onSelectedTabClick,
|
||||
onMove = onMove,
|
||||
)
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onPrivateModeToggleClick,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.mozac_ic_private_mode_24),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
contentDescription = if (state.isPrivateMode) {
|
||||
stringResource(R.string.content_description_disable_private_browsing_button)
|
||||
} else {
|
||||
stringResource(R.string.content_description_private_browsing_button)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAddTabClick) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.mozac_ic_plus_24),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
contentDescription = stringResource(R.string.add_tab),
|
||||
TabsList(
|
||||
state = state,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
onCloseTabClick = onCloseTabClick,
|
||||
onSelectedTabClick = onSelectedTabClick,
|
||||
onMove = onMove,
|
||||
)
|
||||
|
||||
IconButton(onClick = onAddTabClick) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.mozac_ic_plus_24),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
contentDescription = stringResource(R.string.add_tab),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TabStripTabCounterButton(
|
||||
tabCount = state.tabs.size,
|
||||
size = dimensionResource(R.dimen.tab_strip_height),
|
||||
menuItems = state.menuItems,
|
||||
onClick = onTabCounterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +246,7 @@ private fun TabsList(
|
||||
val listState = rememberLazyListState()
|
||||
// Calculate the width of each tab item based on available width and the number of tabs and
|
||||
// taking into account the space between tabs.
|
||||
val availableWidth = maxWidth - tabStripStartPadding
|
||||
val availableWidth = maxWidth - tabStripListContentStartPadding
|
||||
val tabWidth = (availableWidth / state.tabs.size) - spaceBetweenTabs
|
||||
|
||||
val reorderState = createListReorderState(
|
||||
@ -232,7 +270,7 @@ private fun TabsList(
|
||||
)
|
||||
.selectableGroup(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(start = tabStripStartPadding),
|
||||
contentPadding = PaddingValues(start = tabStripListContentStartPadding),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.tabs,
|
||||
@ -428,6 +466,36 @@ private fun TabStripIcon(
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeTab(
|
||||
numberOfTabs: Int,
|
||||
isPrivate: Boolean,
|
||||
tabsUseCases: TabsUseCases,
|
||||
tabId: String,
|
||||
onLastTabClose: (isPrivate: Boolean) -> Unit,
|
||||
onCloseTabClick: (isPrivate: Boolean) -> Unit,
|
||||
) {
|
||||
if (numberOfTabs == 1) {
|
||||
onLastTabClose(isPrivate)
|
||||
}
|
||||
tabsUseCases.removeTab(tabId)
|
||||
onCloseTabClick(isPrivate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoking the callback is required so the caller can update the browsing mode in cases where
|
||||
* appStore.dispatch(AppAction.ModeChange(newMode)) is not enough. This bug is tracked here:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1923650
|
||||
*/
|
||||
private fun toggleBrowsingMode(
|
||||
isCurrentModePrivate: Boolean,
|
||||
onPrivateModeToggleClick: (mode: BrowsingMode) -> Unit,
|
||||
appStore: AppStore,
|
||||
) {
|
||||
val newMode = BrowsingMode.fromBoolean(!isCurrentModePrivate)
|
||||
onPrivateModeToggleClick(newMode)
|
||||
appStore.dispatch(AppAction.ModeChange(newMode))
|
||||
}
|
||||
|
||||
private class TabUIStateParameterProvider : PreviewParameterProvider<TabStripState> {
|
||||
override val values: Sequence<TabStripState>
|
||||
get() = sequenceOf(
|
||||
@ -470,6 +538,7 @@ private class TabUIStateParameterProvider : PreviewParameterProvider<TabStripSta
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -516,12 +585,14 @@ private fun TabStripContentPreview(tabs: List<TabStripItem>) {
|
||||
state = TabStripState(
|
||||
tabs = tabs,
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = emptyList(),
|
||||
),
|
||||
onAddTabClick = {},
|
||||
onPrivateModeToggleClick = {},
|
||||
onCloseTabClick = { _, _ -> },
|
||||
onSelectedTabClick = {},
|
||||
onMove = { _, _, _ -> },
|
||||
onTabCounterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -559,6 +630,7 @@ private fun TabStripPreview() {
|
||||
onCloseTabClick = {},
|
||||
onSelectedTabClick = {},
|
||||
onPrivateModeToggleClick = {},
|
||||
onTabCounterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,24 @@ import mozilla.components.browser.state.state.TabSessionState
|
||||
*
|
||||
* @property tabs The list of [TabStripItem].
|
||||
* @property isPrivateMode Whether or not the browser is in private mode.
|
||||
* @property tabCounterMenuItems The list of [TabCounterMenuItem]s to be displayed in the tab
|
||||
* counter menu.
|
||||
*/
|
||||
data class TabStripState(
|
||||
val tabs: List<TabStripItem>,
|
||||
val isPrivateMode: Boolean,
|
||||
val tabCounterMenuItems: List<TabCounterMenuItem>,
|
||||
) {
|
||||
|
||||
val menuItems
|
||||
get() = tabCounterMenuItems.map { it.toMenuItem() }
|
||||
|
||||
companion object {
|
||||
val initial = TabStripState(tabs = emptyList(), isPrivateMode = false)
|
||||
val initial = TabStripState(
|
||||
tabs = emptyList(),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +62,16 @@ data class TabStripItem(
|
||||
*
|
||||
* @param isSelectDisabled When true, the tabs will show as unselected.
|
||||
* @param isPossiblyPrivateMode Whether or not the browser is in private mode.
|
||||
* @param addTab Invoked when conditions are met for adding a new normal browsing mode tab.
|
||||
* @param toggleBrowsingMode Invoked when conditions are met for toggling the browsing mode.
|
||||
* @param closeTab Invoked when close tab is clicked.
|
||||
*/
|
||||
internal fun BrowserState.toTabStripState(
|
||||
isSelectDisabled: Boolean,
|
||||
isPossiblyPrivateMode: Boolean,
|
||||
addTab: () -> Unit,
|
||||
toggleBrowsingMode: (isCurrentlyPrivate: Boolean) -> Unit,
|
||||
closeTab: (isPrivate: Boolean, numberOfTabs: Int) -> Unit,
|
||||
): TabStripState {
|
||||
val isPrivateMode = if (isSelectDisabled) {
|
||||
isPossiblyPrivateMode
|
||||
@ -62,8 +79,10 @@ internal fun BrowserState.toTabStripState(
|
||||
selectedTab?.content?.private == true
|
||||
}
|
||||
|
||||
val tabs = getNormalOrPrivateTabs(private = isPrivateMode)
|
||||
|
||||
return TabStripState(
|
||||
tabs = getNormalOrPrivateTabs(private = isPrivateMode)
|
||||
tabs = tabs
|
||||
.map {
|
||||
it.toTabStripItem(
|
||||
isSelectDisabled = isSelectDisabled,
|
||||
@ -71,9 +90,57 @@ internal fun BrowserState.toTabStripState(
|
||||
)
|
||||
},
|
||||
isPrivateMode = isPrivateMode,
|
||||
tabCounterMenuItems = mapToMenuItems(
|
||||
isSelectEnabled = !isSelectDisabled,
|
||||
isPrivateMode = isPrivateMode,
|
||||
addTab = addTab,
|
||||
toggleBrowsingMode = toggleBrowsingMode,
|
||||
closeTab = closeTab,
|
||||
numberOfTabs = tabs.size,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapToMenuItems(
|
||||
isSelectEnabled: Boolean,
|
||||
isPrivateMode: Boolean,
|
||||
toggleBrowsingMode: (isCurrentlyPrivate: Boolean) -> Unit,
|
||||
addTab: () -> Unit,
|
||||
closeTab: (isPrivate: Boolean, numberOfTabs: Int) -> Unit,
|
||||
numberOfTabs: Int,
|
||||
): List<TabCounterMenuItem> = buildList {
|
||||
if (isSelectEnabled || isPrivateMode) {
|
||||
val onClick = {
|
||||
if (isPrivateMode) {
|
||||
toggleBrowsingMode(true)
|
||||
} else {
|
||||
addTab()
|
||||
}
|
||||
}
|
||||
add(TabCounterMenuItem.IconItem.NewTab(onClick = onClick))
|
||||
}
|
||||
|
||||
if (isSelectEnabled || !isPrivateMode) {
|
||||
val onClick = {
|
||||
if (isPrivateMode) {
|
||||
addTab()
|
||||
} else {
|
||||
toggleBrowsingMode(false)
|
||||
}
|
||||
}
|
||||
add(TabCounterMenuItem.IconItem.NewPrivateTab(onClick = onClick))
|
||||
}
|
||||
|
||||
if (isSelectEnabled) {
|
||||
add(TabCounterMenuItem.Divider)
|
||||
add(
|
||||
TabCounterMenuItem.IconItem.CloseTab(
|
||||
onClick = { closeTab(isPrivateMode, numberOfTabs) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TabSessionState.toTabStripItem(
|
||||
isSelectDisabled: Boolean,
|
||||
selectedTabId: String?,
|
||||
|
@ -0,0 +1,129 @@
|
||||
/* 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.browser.tabstrip
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mozilla.fenix.compose.TabCounter
|
||||
import org.mozilla.fenix.compose.menu.DropdownMenu
|
||||
import org.mozilla.fenix.compose.menu.MenuItem
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* A button showing number of tabs in the tab strip, encapsulating [TabCounter] and [DropdownMenu].
|
||||
* When long pressed, the [DropdownMenu] will appear.
|
||||
*
|
||||
* @param tabCount The number of tabs to display in the counter.
|
||||
* @param size The size of the button.
|
||||
* @param menuItems The list of [MenuItem] to display in the dropdown menu.
|
||||
* @param modifier The [modifier] applied to the composable.
|
||||
* @param onClick Invoked when the user clicks the button.
|
||||
*/
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun TabStripTabCounterButton(
|
||||
tabCount: Int,
|
||||
size: Dp,
|
||||
menuItems: List<MenuItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
var menuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
onLongClick = {
|
||||
menuExpanded = true
|
||||
},
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TabCounter(
|
||||
tabCount = tabCount,
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
menuItems = menuItems,
|
||||
expanded = menuExpanded,
|
||||
offset = DpOffset(
|
||||
x = 0.dp,
|
||||
y = -size,
|
||||
),
|
||||
onDismissRequest = { menuExpanded = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TabStripTabCounterButtonPreview() {
|
||||
FirefoxTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FirefoxTheme.colors.layer1)
|
||||
.padding(FirefoxTheme.space.baseContentEqualPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(FirefoxTheme.space.baseContentEqualPadding),
|
||||
) {
|
||||
Text(
|
||||
text = "TabStripTabCounterButton",
|
||||
style = FirefoxTheme.typography.body1,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = """
|
||||
Clicking the button will increment the tab count. Long press the button to open the dropdown menu.
|
||||
""".trimIndent(),
|
||||
style = FirefoxTheme.typography.caption,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
var tabCount by remember { mutableIntStateOf(1) }
|
||||
TabStripTabCounterButton(
|
||||
tabCount = tabCount,
|
||||
size = 56.dp,
|
||||
menuItems = listOf(
|
||||
TabCounterMenuItem.IconItem.NewTab { },
|
||||
TabCounterMenuItem.IconItem.NewPrivateTab { },
|
||||
TabCounterMenuItem.Divider,
|
||||
TabCounterMenuItem.IconItem.CloseTab { },
|
||||
).map { it.toMenuItem() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.background(FirefoxTheme.colors.layer2),
|
||||
onClick = { tabCount++ },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -165,9 +165,8 @@ class DefaultToolbarIntegration(
|
||||
addShareBrowserAction()
|
||||
} else {
|
||||
addNewTabBrowserAction()
|
||||
addTabCounterBrowserAction()
|
||||
}
|
||||
|
||||
addTabCounterBrowserAction()
|
||||
}
|
||||
|
||||
private fun addNewTabBrowserAction() {
|
||||
|
@ -1230,6 +1230,7 @@ class HomeFragment : Fragment() {
|
||||
onPrivateModeToggleClick = { mode ->
|
||||
browsingModeManager.mode = mode
|
||||
},
|
||||
onTabCounterClick = { openTabsTray() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1558,7 +1559,12 @@ class HomeFragment : Fragment() {
|
||||
private fun openTabsTray() {
|
||||
findNavController().nav(
|
||||
R.id.homeFragment,
|
||||
HomeFragmentDirections.actionGlobalTabsTrayFragment(),
|
||||
HomeFragmentDirections.actionGlobalTabsTrayFragment(
|
||||
page = when (browsingModeManager.mode) {
|
||||
BrowsingMode.Normal -> Page.NormalTabs
|
||||
BrowsingMode.Private -> Page.PrivateTabs
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -88,17 +88,24 @@ class ToolbarView(
|
||||
* @param browserState [BrowserState] is used to update tab counter's state.
|
||||
*/
|
||||
fun updateButtonVisibility(browserState: BrowserState) {
|
||||
val showTabCounterAndMenu = !context.shouldAddNavigationBar()
|
||||
binding.menuButton.isVisible = showTabCounterAndMenu
|
||||
binding.tabButton.isVisible = showTabCounterAndMenu
|
||||
val shouldAddNavigationBar = context.shouldAddNavigationBar()
|
||||
val showMenu = !shouldAddNavigationBar
|
||||
val showTabCounter = !(shouldAddNavigationBar || context.isTabStripEnabled())
|
||||
binding.menuButton.isVisible = showMenu
|
||||
binding.tabButton.isVisible = showTabCounter
|
||||
|
||||
if (showTabCounterAndMenu) {
|
||||
homeMenuView = buildHomeMenu()
|
||||
tabCounterView = buildTabCounter()
|
||||
tabCounterView?.update(browserState)
|
||||
tabCounterView = if (showTabCounter) {
|
||||
buildTabCounter().also {
|
||||
it.update(browserState)
|
||||
}
|
||||
} else {
|
||||
homeMenuView = null
|
||||
tabCounterView = null
|
||||
null
|
||||
}
|
||||
|
||||
homeMenuView = if (showMenu) {
|
||||
buildHomeMenu()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
/* 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.browser.tabstrip
|
||||
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
@ -10,11 +14,21 @@ class TabStripStateTest {
|
||||
@Test
|
||||
fun `WHEN browser state tabs is empty THEN tabs strip state tabs is empty`() {
|
||||
val browserState = BrowserState(tabs = emptyList())
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = false)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(tabs = emptyList(), false)
|
||||
val expected = TabStripState(
|
||||
tabs = emptyList(),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -42,7 +56,14 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "1",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = false)
|
||||
val actual =
|
||||
browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -62,9 +83,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -91,7 +113,13 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = true, isPossiblyPrivateMode = true)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = true,
|
||||
isPossiblyPrivateMode = true,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -111,9 +139,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = true,
|
||||
tabCounterMenuItems = noTabSelectedPrivateModeMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -141,7 +170,13 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "1",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = true)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = true,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -154,9 +189,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -178,7 +214,13 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "2",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = false)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -198,9 +240,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -228,7 +271,13 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "2",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = false)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -248,9 +297,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = true,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -272,7 +322,13 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "2",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = true, isPossiblyPrivateMode = false)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = true,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -292,9 +348,10 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = noTabSelectedNormalModeMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -316,7 +373,13 @@ class TabStripStateTest {
|
||||
),
|
||||
selectedTabId = "2",
|
||||
)
|
||||
val actual = browserState.toTabStripState(isSelectDisabled = false, isPossiblyPrivateMode = false)
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = {},
|
||||
toggleBrowsingMode = {},
|
||||
closeTab = { _, _ -> },
|
||||
)
|
||||
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
@ -336,8 +399,123 @@ class TabStripStateTest {
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = allMenuItems,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
expected isSameAs actual
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN menu items are clicked THEN the correct action is performed`() {
|
||||
var addTabClicked = false
|
||||
var shouldOpenPrivateTab: Boolean? = null
|
||||
var toggleBrowsingModeClicked = false
|
||||
var closeTabClicked = false
|
||||
var closTabParams: Pair<Boolean, Int>? = null
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(
|
||||
createTab(
|
||||
url = "https://example.com",
|
||||
title = "Example 1",
|
||||
private = false,
|
||||
id = "1",
|
||||
),
|
||||
createTab(
|
||||
url = "https://example2.com",
|
||||
title = "",
|
||||
private = false,
|
||||
id = "2",
|
||||
),
|
||||
),
|
||||
selectedTabId = "2",
|
||||
)
|
||||
val addTab = {
|
||||
addTabClicked = true
|
||||
}
|
||||
val toggleBrowsingMode: (isPrivate: Boolean) -> Unit = {
|
||||
toggleBrowsingModeClicked = true
|
||||
shouldOpenPrivateTab = it
|
||||
}
|
||||
val closeTab: (isPrivate: Boolean, numberOfTabs: Int) -> Unit = { isPrivate, numberOfTabs ->
|
||||
closeTabClicked = true
|
||||
closTabParams = Pair(isPrivate, numberOfTabs)
|
||||
}
|
||||
val actual = browserState.toTabStripState(
|
||||
isSelectDisabled = false,
|
||||
isPossiblyPrivateMode = false,
|
||||
addTab = addTab,
|
||||
toggleBrowsingMode = toggleBrowsingMode,
|
||||
closeTab = closeTab,
|
||||
)
|
||||
|
||||
val newTab = TabCounterMenuItem.IconItem.NewTab(onClick = addTab)
|
||||
val newPrivateTab =
|
||||
TabCounterMenuItem.IconItem.NewPrivateTab(onClick = { toggleBrowsingMode(true) })
|
||||
val closeTabItem = TabCounterMenuItem.IconItem.CloseTab(onClick = { closeTab(false, 2) })
|
||||
val expected = TabStripState(
|
||||
tabs = listOf(
|
||||
TabStripItem(
|
||||
id = "1",
|
||||
title = "Example 1",
|
||||
url = "https://example.com",
|
||||
isSelected = false,
|
||||
isPrivate = false,
|
||||
),
|
||||
TabStripItem(
|
||||
id = "2",
|
||||
title = "https://example2.com",
|
||||
url = "https://example2.com",
|
||||
isSelected = true,
|
||||
isPrivate = false,
|
||||
),
|
||||
),
|
||||
isPrivateMode = false,
|
||||
tabCounterMenuItems = listOf(
|
||||
newTab,
|
||||
newPrivateTab,
|
||||
TabCounterMenuItem.Divider,
|
||||
closeTabItem,
|
||||
),
|
||||
)
|
||||
|
||||
expected isSameAs actual
|
||||
|
||||
newTab.onClick()
|
||||
assertEquals(true, addTabClicked)
|
||||
newPrivateTab.onClick()
|
||||
assertEquals(true, shouldOpenPrivateTab)
|
||||
assertEquals(true, toggleBrowsingModeClicked)
|
||||
closeTabItem.onClick()
|
||||
assertEquals(true, closeTabClicked)
|
||||
assertEquals(Pair(false, 2), closTabParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the [TabStripState] is the same as the [other] [TabStripState] by comparing
|
||||
* their properties as assertEquals does. This ignores the lambda references in the
|
||||
* [TabCounterMenuItem.IconItem]s as asserting them is not straightforward.
|
||||
*/
|
||||
private infix fun TabStripState.isSameAs(other: TabStripState) {
|
||||
assertEquals(tabs, other.tabs)
|
||||
assertEquals(isPrivateMode, other.isPrivateMode)
|
||||
assertEquals(
|
||||
tabCounterMenuItems.map { it.javaClass },
|
||||
other.tabCounterMenuItems.map { it.javaClass },
|
||||
)
|
||||
}
|
||||
|
||||
private val allMenuItems = listOf(
|
||||
TabCounterMenuItem.IconItem.NewTab(onClick = {}),
|
||||
TabCounterMenuItem.IconItem.NewPrivateTab(onClick = {}),
|
||||
TabCounterMenuItem.Divider,
|
||||
TabCounterMenuItem.IconItem.CloseTab(onClick = {}),
|
||||
)
|
||||
|
||||
private val noTabSelectedNormalModeMenuItems = listOf(
|
||||
TabCounterMenuItem.IconItem.NewPrivateTab(onClick = {}),
|
||||
)
|
||||
|
||||
private val noTabSelectedPrivateModeMenuItems = listOf(
|
||||
TabCounterMenuItem.IconItem.NewTab(onClick = {}),
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user