tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 54d2798991d79454e080c51301a988f0084039c9
parent fa3257dcab38a906a26add9423a22e9155094c76
Author: rmalicdem <rmalicdem@mozilla.com>
Date:   Mon, 24 Nov 2025 16:11:00 +0000

Bug 1993325 - Extract shared components in menu r=android-reviewers,skhan,android-addons-reviewers,rpl

Differential Revision: https://phabricator.services.mozilla.com/D268650

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt | 27++++++++++++++++-----------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/CustomTabMenu.kt | 16+++++++---------
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsMenuItem.kt | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt | 313+++++++++----------------------------------------------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt | 46++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 383 insertions(+), 298 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt @@ -441,7 +441,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { state.extensionMenuState.webExtensionsCount } - val allWebExtensionsDisabled by store.observeAsState(initialValue = false) { state -> + val isAllWebExtensionsDisabled by store.observeAsState(initialValue = false) { state -> state.extensionMenuState.allWebExtensionsDisabled } @@ -483,11 +483,17 @@ class MenuDialogFragment : BottomSheetDialogFragment() { val extensionsMenuItemDescription = getExtensionsMenuItemDescription( isExtensionsProcessDisabled = isExtensionsProcessDisabled, - allWebExtensionsDisabled = allWebExtensionsDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, availableAddons = availableAddons, browserWebExtensionMenuItems = browserWebExtensionMenuItem, ) + val webExtensionMenuItems = remember(availableAddons, browserWebExtensionMenuItem) { + browserWebExtensionMenuItem.associateWith { menuItem -> + availableAddons.find { addon -> addon.id == menuItem.id } + } + } + BackHandler { this@MenuDialogFragment.dismissAllowingStateLoss() } @@ -582,7 +588,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { showBanner = shouldShowMenuBanner && !defaultBrowser, isDownloadHighlighted = isDownloadHighlighted, webExtensionMenuCount = webExtensionsCount, - allWebExtensionsDisabled = allWebExtensionsDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, onMozillaAccountButtonClick = { store.dispatch( MenuAction.Navigate.MozillaAccount( @@ -620,7 +626,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { }, onExtensionsMenuClick = { if ( - allWebExtensionsDisabled || + isAllWebExtensionsDisabled || isExtensionsProcessDisabled || extensionsMenuItemDescription == null ) { @@ -720,7 +726,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { Addons( accessPoint = args.accesspoint, availableAddons = availableAddons, - webExtensionMenuItems = browserWebExtensionMenuItem, + webExtensionMenuItems = webExtensionMenuItems, addonInstallationInProgress = addonInstallationInProgress, recommendedAddons = recommendedAddons, onAddonClick = { addon -> @@ -778,7 +784,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { isPrivate = isPrivate, isExtensionsExpanded = isExtensionsExpanded, isExtensionsProcessDisabled = isExtensionsProcessDisabled, - isAllWebExtensionsDisabled = allWebExtensionsDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, shouldShowExtensionsMenu = settings.shouldShowCustomTabExtensions, webExtensionMenuCount = webExtensionsCount, extensionsMenuDescription = extensionsMenuItemDescription, @@ -820,14 +826,13 @@ class MenuDialogFragment : BottomSheetDialogFragment() { store.dispatch(MenuAction.Navigate.Share) }, onExtensionsMenuClick = { - if (!allWebExtensionsDisabled && !isExtensionsProcessDisabled) { + if (!isAllWebExtensionsDisabled && !isExtensionsProcessDisabled) { isExtensionsExpanded = !isExtensionsExpanded } }, extensionSubmenu = { CustomTabAddons( - availableAddons = availableAddons, - webExtensionMenuItems = browserWebExtensionMenuItem, + webExtensionMenuItems = webExtensionMenuItems, onWebExtensionMenuItemClick = { Events.browserMenuAction.record( Events.BrowserMenuActionExtra( @@ -848,7 +853,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { private fun getExtensionsMenuItemDescription( isExtensionsProcessDisabled: Boolean, - allWebExtensionsDisabled: Boolean, + isAllWebExtensionsDisabled: Boolean, availableAddons: List<Addon>, browserWebExtensionMenuItems: List<WebExtensionMenuItem>, ): String? { @@ -866,7 +871,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { browserWebExtensionMenuItems.joinToString(separator = ", ") { it.label } } - allWebExtensionsDisabled -> { + isAllWebExtensionsDisabled -> { requireContext().getString(R.string.browser_menu_no_extensions_installed_description) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/CustomTabMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/CustomTabMenu.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -105,7 +104,7 @@ internal fun CustomTabMenu( onStopButtonClick: () -> Unit, onShareButtonClick: () -> Unit, onExtensionsMenuClick: () -> Unit, - extensionSubmenu: @Composable ColumnScope.() -> Unit, + extensionSubmenu: @Composable () -> Unit, ) { MenuFrame( contentModifier = Modifier @@ -224,14 +223,15 @@ internal fun CustomTabMenu( if (shouldShowExtensionsMenu) { ExtensionsMenuItem( + inCustomTab = true, + isPrivate = isPrivate, isExtensionsProcessDisabled = isExtensionsProcessDisabled, isExtensionsExpanded = isExtensionsExpanded, - isPrivate = isPrivate, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, webExtensionMenuCount = webExtensionMenuCount, - allWebExtensionsDisabled = isAllWebExtensionsDisabled, + extensionsMenuItemDescription = extensionsMenuDescription, onExtensionsMenuClick = onExtensionsMenuClick, extensionSubmenu = extensionSubmenu, - extensionsMenuItemDescription = extensionsMenuDescription, ) } } @@ -257,8 +257,7 @@ internal fun CustomTabMenu( @Composable internal fun CustomTabAddons( - availableAddons: List<Addon>, - webExtensionMenuItems: List<WebExtensionMenuItem>, + webExtensionMenuItems: Map<WebExtensionMenuItem, Addon?>, onWebExtensionMenuItemClick: () -> Unit, ) { Column( @@ -269,8 +268,7 @@ internal fun CustomTabAddons( accessPoint = MenuAccessPoint.External, webExtensionMenuItems = webExtensionMenuItems, onWebExtensionMenuItemClick = onWebExtensionMenuItemClick, - availableAddons = availableAddons, - onSettingsClick = {}, + onWebExtensionMenuItemSettingsClick = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsMenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsMenuItem.kt @@ -0,0 +1,279 @@ +/* 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.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import mozilla.components.feature.addons.Addon +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.R +import org.mozilla.fenix.components.menu.MenuAccessPoint +import org.mozilla.fenix.components.menu.MenuDialogTestTag.EXTENSIONS +import org.mozilla.fenix.components.menu.MenuDialogTestTag.EXTENSIONS_OPTION_CHEVRON +import org.mozilla.fenix.components.menu.store.WebExtensionMenuItem +import org.mozilla.fenix.theme.FirefoxTheme +import mozilla.components.ui.icons.R as iconsR + +@Suppress("LongParameterList") +@Composable +internal fun ExtensionsMenuItem( + inCustomTab: Boolean, + isPrivate: Boolean, + isExtensionsProcessDisabled: Boolean, + isExtensionsExpanded: Boolean, + isAllWebExtensionsDisabled: Boolean, + webExtensionMenuCount: Int, + extensionsMenuItemDescription: String?, + onExtensionsMenuClick: () -> Unit, + extensionSubmenu: @Composable () -> Unit, +) { + val stateDescription = stateDescriptionForExtensions( + extensionsMenuItemDescription = extensionsMenuItemDescription, + isExtensionsExpanded = isExtensionsExpanded, + ) + + val beforeIconPainter = beforeIconPainterForExtensions( + isExtensionsProcessDisabled = isExtensionsProcessDisabled, + isPrivate = isPrivate, + ) + + val descriptionState = descriptionStateForExtensions( + isExtensionsProcessDisabled = isExtensionsProcessDisabled, + ) + + val state = menuItemStateForExtensions( + inCustomTab = inCustomTab, + isExtensionsProcessDisabled = isExtensionsProcessDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, + ) + + Column { + MenuItem( + label = stringResource(id = R.string.browser_menu_extensions), + description = extensionsMenuItemDescription, + maxDescriptionLines = 1, + stateDescription = stateDescription, + beforeIconPainter = beforeIconPainter, + onClick = onExtensionsMenuClick, + descriptionState = descriptionState, + modifier = Modifier.semantics { + testTag = EXTENSIONS + testTagsAsResourceId = true + }, + state = state, + ) { + ExtensionsMenuTrailingContent( + inCustomTab = inCustomTab, + isExtensionsProcessDisabled = isExtensionsProcessDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, + isExtensionsExpanded = isExtensionsExpanded, + webExtensionMenuCount = webExtensionMenuCount, + extensionsMenuItemDescription = extensionsMenuItemDescription, + ) + } + + ExpandableMenuItemAnimation( + isExpanded = isExtensionsExpanded, + content = extensionSubmenu, + ) + } +} + +@Composable +private fun ExtensionsMenuTrailingContent( + inCustomTab: Boolean, + isExtensionsProcessDisabled: Boolean, + isAllWebExtensionsDisabled: Boolean, + isExtensionsExpanded: Boolean, + webExtensionMenuCount: Int, + extensionsMenuItemDescription: String?, +) { + if (extensionsMenuItemDescription == null) { + return + } + + if (isExtensionsProcessDisabled || isAllWebExtensionsDisabled) { + if (!inCustomTab) { + Icon( + painter = painterResource(id = iconsR.drawable.mozac_ic_settings_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + return + } + + NumberedChevronBadge( + count = webExtensionMenuCount, + isExpanded = isExtensionsExpanded, + ) +} + +private fun stateDescriptionForExtensions( + extensionsMenuItemDescription: String?, + isExtensionsExpanded: Boolean, +): String = when { + extensionsMenuItemDescription == null -> "" + isExtensionsExpanded -> "Expanded" + else -> "Collapsed" +} + +@Composable +private fun beforeIconPainterForExtensions( + isExtensionsProcessDisabled: Boolean, + isPrivate: Boolean, +) = when { + isExtensionsProcessDisabled && isPrivate -> + painterResource(id = iconsR.drawable.mozac_ic_extension_warning_private_24) + isExtensionsProcessDisabled -> + painterResource(id = iconsR.drawable.mozac_ic_extension_warning_24) + else -> + painterResource(id = iconsR.drawable.mozac_ic_extension_24) +} + +private fun descriptionStateForExtensions( + isExtensionsProcessDisabled: Boolean, +): MenuItemState = when (isExtensionsProcessDisabled) { + true -> MenuItemState.DISABLED + else -> MenuItemState.ENABLED +} + +private fun menuItemStateForExtensions( + inCustomTab: Boolean, + isExtensionsProcessDisabled: Boolean, + isAllWebExtensionsDisabled: Boolean, +): MenuItemState = when { + inCustomTab && (isExtensionsProcessDisabled || isAllWebExtensionsDisabled) -> + MenuItemState.DISABLED + isExtensionsProcessDisabled -> + MenuItemState.CRITICAL + else -> + MenuItemState.ENABLED +} + +@Composable +private fun NumberedChevronBadge( + count: Int, + isExpanded: Boolean, +) { + Row( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = RoundedCornerShape(16.dp), + ) + .padding( + start = if (count > 0) 8.dp else 2.dp, + top = 2.dp, + bottom = 2.dp, + end = 2.dp, + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (count > 0) { + Text( + text = count.toString(), + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Ellipsis, + style = FirefoxTheme.typography.caption, + maxLines = 1, + ) + } + + Icon( + painter = if (isExpanded) { + painterResource(id = iconsR.drawable.mozac_ic_chevron_up_20) + } else { + painterResource(id = iconsR.drawable.mozac_ic_chevron_down_20) + }, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.semantics { + testTagsAsResourceId = true + testTag = EXTENSIONS_OPTION_CHEVRON + }, + ) + } +} + +@Composable +internal fun WebExtensionMenuItems( + accessPoint: MenuAccessPoint, + webExtensionMenuItems: Map<WebExtensionMenuItem, Addon?>, + onWebExtensionMenuItemClick: () -> Unit, + onWebExtensionMenuItemSettingsClick: (Addon) -> Unit, +) { + Column( + modifier = Modifier + .padding(top = 2.dp) + .semantics { + collectionInfo = CollectionInfo( + rowCount = webExtensionMenuItems.size, + columnCount = 1, + ) + }, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + for (webExtensionMenuItem in webExtensionMenuItems) { + val extension = webExtensionMenuItem.key + val addon = webExtensionMenuItem.value + + WebExtensionMenuItem( + label = extension.label, + iconPainter = extension.icon?.let { icon -> + BitmapPainter(image = icon.asImageBitmap()) + } + ?: painterResource(iconsR.drawable.mozac_ic_web_extension_default_icon), + iconTint = when (extension.icon) { + null -> MaterialTheme.colorScheme.onSurface + else -> Color.Unspecified + }, + enabled = extension.enabled, + badgeText = extension.badgeText, + onClick = { + if (accessPoint != MenuAccessPoint.External) { + onWebExtensionMenuItemClick() + extension.onClick() + } else { + // TODO(Bug 1959344): CustomTab should be visible to add-ons through the tabs API + // and it should be detected as active tab while in foreground. + Logger.error( + message = "WebExtensionsMenuItem does not does not support onClick in CustomTab mode", + throwable = NotImplementedError(), + ) + } + }, + onSettingsClick = if (accessPoint != MenuAccessPoint.External) { + { addon?.let { onWebExtensionMenuItemSettingsClick(it) } } + } else { + null + }, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt @@ -4,13 +4,6 @@ package org.mozilla.fenix.components.menu.compose -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -18,7 +11,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -32,15 +24,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -54,7 +43,6 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter @@ -67,19 +55,15 @@ import mozilla.components.feature.addons.ui.summary import mozilla.components.service.fxa.manager.AccountState import mozilla.components.service.fxa.manager.AccountState.AuthenticationProblem import mozilla.components.service.fxa.store.Account -import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.R import org.mozilla.fenix.components.menu.MenuAccessPoint import org.mozilla.fenix.components.menu.MenuDialogTestTag.DESKTOP_SITE_OFF import org.mozilla.fenix.components.menu.MenuDialogTestTag.DESKTOP_SITE_ON -import org.mozilla.fenix.components.menu.MenuDialogTestTag.EXTENSIONS -import org.mozilla.fenix.components.menu.MenuDialogTestTag.EXTENSIONS_OPTION_CHEVRON import org.mozilla.fenix.components.menu.MenuDialogTestTag.MORE_OPTION_CHEVRON import org.mozilla.fenix.components.menu.compose.header.MozillaAccountMenuItem import org.mozilla.fenix.components.menu.store.WebExtensionMenuItem import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme -import org.mozilla.fenix.utils.DURATION_MS_MAIN_MENU_ITEM import mozilla.components.ui.icons.R as iconsR /** @@ -102,7 +86,7 @@ import mozilla.components.ui.icons.R as iconsR * @param isReaderViewActive Whether or not Reader View is active or not. * @param isExtensionsProcessDisabled Whether or not the extensions process is disabled due to extension errors. * @param isMoreMenuHighlighted Whether or not the more menu icon is highlighted. - * @param allWebExtensionsDisabled Whether or not all web extensions are disabled. + * @param isAllWebExtensionsDisabled Whether or not all web extensions are disabled. * @param canGoBack Whether or not the back button is enabled. * @param canGoForward Whether or not the forward button is enabled. * @param scrollState The [ScrollState] used for vertical scrolling. @@ -131,9 +115,9 @@ import mozilla.components.ui.icons.R as iconsR * @param onRefreshButtonClick Invoked when the user clicks on the refresh button. * @param onStopButtonClick Invoked when the user clicks on the stop button. * @param onShareButtonClick Invoked when the user clicks on the share button. + * @param extensionsMenuItemDescription The label of extensions menu item description. * @param moreSettingsSubmenu The content of more menu item. * @param extensionSubmenu The content of extensions menu item to avoid configuration during animation. - * @param extensionsMenuItemDescription The label of extensions menu item description. */ @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod", "CognitiveComplexMethod") @Composable @@ -154,7 +138,7 @@ fun MainMenu( isReaderViewActive: Boolean, isExtensionsProcessDisabled: Boolean, isMoreMenuHighlighted: Boolean, - allWebExtensionsDisabled: Boolean, + isAllWebExtensionsDisabled: Boolean, canGoBack: Boolean, canGoForward: Boolean, scrollState: ScrollState, @@ -182,9 +166,9 @@ fun MainMenu( onRefreshButtonClick: (longPress: Boolean) -> Unit, onStopButtonClick: () -> Unit, onShareButtonClick: () -> Unit, - moreSettingsSubmenu: @Composable ColumnScope.() -> Unit, - extensionSubmenu: @Composable ColumnScope.() -> Unit, extensionsMenuItemDescription: String?, + moreSettingsSubmenu: @Composable () -> Unit, + extensionSubmenu: @Composable () -> Unit, ) { MenuFrame( contentModifier = Modifier @@ -270,16 +254,19 @@ fun MainMenu( } if (accessPoint == MenuAccessPoint.Home) { - HomepageMenuGroup( - onExtensionsMenuClick = onExtensionsMenuClick, - isExtensionsProcessDisabled = isExtensionsProcessDisabled, - isExtensionsExpanded = isExtensionsExpanded, - isPrivate = isPrivate, - webExtensionMenuCount = webExtensionMenuCount, - allWebExtensionsDisabled = allWebExtensionsDisabled, - extensionSubmenu = extensionSubmenu, - extensionsMenuItemDescription = extensionsMenuItemDescription, - ) + MenuGroup { + ExtensionsMenuItem( + inCustomTab = false, + isPrivate = isPrivate, + isExtensionsProcessDisabled = isExtensionsProcessDisabled, + isExtensionsExpanded = isExtensionsExpanded, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, + webExtensionMenuCount = webExtensionMenuCount, + extensionsMenuItemDescription = extensionsMenuItemDescription, + onExtensionsMenuClick = onExtensionsMenuClick, + extensionSubmenu = extensionSubmenu, + ) + } } if (accessPoint == MenuAccessPoint.Browser) { @@ -293,17 +280,17 @@ fun MainMenu( isMoreMenuHighlighted = isMoreMenuHighlighted, moreMenuExpanded = isMoreMenuExpanded, webExtensionMenuCount = webExtensionMenuCount, - allWebExtensionsDisabled = allWebExtensionsDisabled, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, onExtensionsMenuClick = onExtensionsMenuClick, onBookmarkPageMenuClick = onBookmarkPageMenuClick, onEditBookmarkButtonClick = onEditBookmarkButtonClick, onSwitchToDesktopSiteMenuClick = onSwitchToDesktopSiteMenuClick, onFindInPageMenuClick = onFindInPageMenuClick, onMoreMenuClick = onMoreMenuClick, + extensionsMenuItemDescription = extensionsMenuItemDescription, moreSettingsSubmenu = moreSettingsSubmenu, extensionSubmenu = extensionSubmenu, - extensionsMenuItemDescription = extensionsMenuItemDescription, - ) + ) } LibraryMenuGroup( @@ -337,153 +324,6 @@ fun MainMenu( } } -@Suppress("LongParameterList", "LongMethod", "CognitiveComplexMethod") -@Composable -internal fun ExtensionsMenuItem( - isExtensionsProcessDisabled: Boolean, - isExtensionsExpanded: Boolean, - isPrivate: Boolean, - webExtensionMenuCount: Int, - allWebExtensionsDisabled: Boolean, - onExtensionsMenuClick: () -> Unit, - extensionSubmenu: @Composable ColumnScope.() -> Unit, - extensionsMenuItemDescription: String?, -) { - Column { - val leftPadding = if (webExtensionMenuCount > 0) 8.dp else 2.dp - MenuItem( - label = stringResource(id = R.string.browser_menu_extensions), - description = extensionsMenuItemDescription, - maxDescriptionLines = 1, - stateDescription = if ( - isExtensionsProcessDisabled || - allWebExtensionsDisabled || - extensionsMenuItemDescription == null - ) { - "" - } else if (isExtensionsExpanded) { - "Expanded" - } else { - "Collapsed" - }, - beforeIconPainter = if (isExtensionsProcessDisabled && isPrivate) { - painterResource(id = iconsR.drawable.mozac_ic_extension_warning_private_24) - } else if (isExtensionsProcessDisabled) { - painterResource(id = iconsR.drawable.mozac_ic_extension_warning_24) - } else { - painterResource(id = iconsR.drawable.mozac_ic_extension_24) - }, - onClick = onExtensionsMenuClick, - descriptionState = if (isExtensionsProcessDisabled) { - MenuItemState.WARNING - } else { - MenuItemState.ENABLED - }, - modifier = Modifier.semantics { - testTag = EXTENSIONS - testTagsAsResourceId = true - }, - state = if (isExtensionsProcessDisabled) { - MenuItemState.CRITICAL - } else { - MenuItemState.ENABLED - }, - ) { - if (extensionsMenuItemDescription == null) { - return@MenuItem - } - - if (isExtensionsProcessDisabled || allWebExtensionsDisabled) { - Icon( - painter = painterResource(id = iconsR.drawable.mozac_ic_settings_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - return@MenuItem - } - - Row( - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = RoundedCornerShape(16.dp), - ) - .padding(start = leftPadding, top = 2.dp, bottom = 2.dp, end = 2.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (webExtensionMenuCount > 0) { - Text( - text = webExtensionMenuCount.toString(), - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - style = FirefoxTheme.typography.caption, - maxLines = 1, - ) - } - - Icon( - painter = if (isExtensionsExpanded) { - painterResource(id = iconsR.drawable.mozac_ic_chevron_up_20) - } else { - painterResource(id = iconsR.drawable.mozac_ic_chevron_down_20) - }, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.semantics { - testTagsAsResourceId = true - testTag = EXTENSIONS_OPTION_CHEVRON - }, - - ) - } - } - - MenuItemAnimation( - isExpanded = isExtensionsExpanded, - submenu = extensionSubmenu, - ) - } -} - -@Composable -private fun MenuItemAnimation( - isExpanded: Boolean, - submenu: @Composable ColumnScope.() -> Unit, -) { - AnimatedVisibility( - visible = isExpanded, - enter = expandVertically( - expandFrom = Alignment.Top, - animationSpec = tween( - durationMillis = DURATION_MS_MAIN_MENU_ITEM, - easing = LinearEasing, - ), - ) + fadeIn( - animationSpec = tween( - durationMillis = DURATION_MS_MAIN_MENU_ITEM, - easing = LinearEasing, - ), - ), - exit = shrinkVertically( - shrinkTowards = Alignment.Top, - animationSpec = tween( - durationMillis = DURATION_MS_MAIN_MENU_ITEM, - easing = LinearEasing, - ), - ) + fadeOut( - animationSpec = tween( - durationMillis = DURATION_MS_MAIN_MENU_ITEM, - easing = LinearEasing, - ), - ), - ) { - Column { - submenu() - } - } -} - @Composable private fun QuitMenuGroup( onQuitMenuClick: () -> Unit, @@ -513,16 +353,16 @@ private fun ToolsAndActionsMenuGroup( isMoreMenuHighlighted: Boolean, moreMenuExpanded: Boolean, webExtensionMenuCount: Int, - allWebExtensionsDisabled: Boolean, + isAllWebExtensionsDisabled: Boolean, onExtensionsMenuClick: () -> Unit, onBookmarkPageMenuClick: () -> Unit, onEditBookmarkButtonClick: () -> Unit, onSwitchToDesktopSiteMenuClick: () -> Unit, onFindInPageMenuClick: () -> Unit, onMoreMenuClick: () -> Unit, - moreSettingsSubmenu: @Composable ColumnScope.() -> Unit, - extensionSubmenu: @Composable ColumnScope.() -> Unit, extensionsMenuItemDescription: String?, + moreSettingsSubmenu: @Composable () -> Unit, + extensionSubmenu: @Composable () -> Unit, ) { MenuGroup { val labelId = R.string.browser_menu_desktop_site @@ -587,14 +427,15 @@ private fun ToolsAndActionsMenuGroup( } ExtensionsMenuItem( + inCustomTab = false, + isPrivate = isPrivate, isExtensionsProcessDisabled = isExtensionsProcessDisabled, isExtensionsExpanded = isExtensionsExpanded, - isPrivate = isPrivate, + isAllWebExtensionsDisabled = isAllWebExtensionsDisabled, webExtensionMenuCount = webExtensionMenuCount, - allWebExtensionsDisabled = allWebExtensionsDisabled, + extensionsMenuItemDescription = extensionsMenuItemDescription, onExtensionsMenuClick = onExtensionsMenuClick, extensionSubmenu = extensionSubmenu, - extensionsMenuItemDescription = extensionsMenuItemDescription, ) if (!moreMenuExpanded) { @@ -604,9 +445,9 @@ private fun ToolsAndActionsMenuGroup( ) } - MenuItemAnimation( + ExpandableMenuItemAnimation( isExpanded = moreMenuExpanded, - submenu = moreSettingsSubmenu, + content = moreSettingsSubmenu, ) } } @@ -736,36 +577,10 @@ private fun LibraryMenuGroup( @Suppress("LongParameterList") @Composable -private fun HomepageMenuGroup( - isExtensionsProcessDisabled: Boolean, - isExtensionsExpanded: Boolean, - isPrivate: Boolean, - webExtensionMenuCount: Int, - allWebExtensionsDisabled: Boolean, - onExtensionsMenuClick: () -> Unit, - extensionSubmenu: @Composable ColumnScope.() -> Unit, - extensionsMenuItemDescription: String?, -) { - MenuGroup { - ExtensionsMenuItem( - isExtensionsProcessDisabled = isExtensionsProcessDisabled, - isExtensionsExpanded = isExtensionsExpanded, - isPrivate = isPrivate, - webExtensionMenuCount = webExtensionMenuCount, - allWebExtensionsDisabled = allWebExtensionsDisabled, - onExtensionsMenuClick = onExtensionsMenuClick, - extensionSubmenu = extensionSubmenu, - extensionsMenuItemDescription = extensionsMenuItemDescription, - ) - } -} - -@Suppress("LongParameterList") -@Composable internal fun Addons( accessPoint: MenuAccessPoint, availableAddons: List<Addon>, - webExtensionMenuItems: List<WebExtensionMenuItem>, + webExtensionMenuItems: Map<WebExtensionMenuItem, Addon?>, addonInstallationInProgress: Addon?, recommendedAddons: List<Addon>, onAddonSettingsClick: (Addon) -> Unit, @@ -793,8 +608,7 @@ internal fun Addons( accessPoint = accessPoint, webExtensionMenuItems = webExtensionMenuItems, onWebExtensionMenuItemClick = onWebExtensionMenuItemClick, - availableAddons = availableAddons, - onSettingsClick = { onAddonSettingsClick(it) }, + onWebExtensionMenuItemSettingsClick = onAddonSettingsClick, ) } else if (recommendedAddons.isNotEmpty()) { AddonsMenuItems( @@ -861,63 +675,6 @@ private fun AddonsMenuItems( } @Composable -internal fun WebExtensionMenuItems( - accessPoint: MenuAccessPoint, - webExtensionMenuItems: List<WebExtensionMenuItem>, - onWebExtensionMenuItemClick: () -> Unit, - availableAddons: List<Addon> = emptyList(), - onSettingsClick: (Addon) -> Unit, -) { - Column( - modifier = Modifier - .padding(top = 2.dp) - .semantics { - collectionInfo = CollectionInfo( - rowCount = availableAddons.size, - columnCount = 1, - ) - }, - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - for (webExtensionMenuItem in webExtensionMenuItems) { - val addon = availableAddons.find { it.id == webExtensionMenuItem.id } - - WebExtensionMenuItem( - label = webExtensionMenuItem.label, - iconPainter = webExtensionMenuItem.icon?.let { icon -> - BitmapPainter(image = icon.asImageBitmap()) - } - ?: painterResource(iconsR.drawable.mozac_ic_web_extension_default_icon), - iconTint = when (webExtensionMenuItem.icon) { - null -> MaterialTheme.colorScheme.onSurface - else -> Color.Unspecified - }, - enabled = webExtensionMenuItem.enabled, - badgeText = webExtensionMenuItem.badgeText, - onClick = { - if (accessPoint != MenuAccessPoint.External) { - onWebExtensionMenuItemClick() - webExtensionMenuItem.onClick() - } else { - // TODO(Bug 1959344): CustomTab should be visible to add-ons through the tabs API - // and it should be detected as active tab while in foreground. - Logger.error( - message = "WebExtensionsMenuItem does not does not support onClick in CustomTab mode", - throwable = NotImplementedError(), - ) - } - }, - onSettingsClick = if (accessPoint != MenuAccessPoint.External) { - { addon?.let { onSettingsClick(addon) } } - } else { - null - }, - ) - } - } -} - -@Composable private fun MoreExtensionsMenuItem( onClick: () -> Unit, label: String, @@ -971,7 +728,7 @@ private fun MenuDialogPreview() { isReaderViewActive = false, isExtensionsProcessDisabled = true, isMoreMenuHighlighted = false, - allWebExtensionsDisabled = false, + isAllWebExtensionsDisabled = false, canGoBack = true, canGoForward = true, extensionsMenuItemDescription = "No extensions enabled", @@ -1037,7 +794,7 @@ private fun MenuDialogPrivatePreview( isMoreMenuHighlighted = false, canGoBack = true, canGoForward = true, - allWebExtensionsDisabled = false, + isAllWebExtensionsDisabled = false, extensionsMenuItemDescription = "No extensions enabled", scrollState = ScrollState(0), showBanner = true, @@ -1069,7 +826,7 @@ private fun MenuDialogPrivatePreview( Addons( accessPoint = MenuAccessPoint.Home, availableAddons = listOf(), - webExtensionMenuItems = listOf(), + webExtensionMenuItems = mapOf(), addonInstallationInProgress = null, recommendedAddons = listOf( Addon( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt @@ -4,6 +4,13 @@ package org.mozilla.fenix.components.menu.compose +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -52,6 +59,7 @@ import org.mozilla.fenix.components.menu.MenuDialogTestTag.WEB_EXTENSION_ITEM import org.mozilla.fenix.compose.list.IconListItem import org.mozilla.fenix.compose.list.TextListItem import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.utils.DURATION_MS_MAIN_MENU_ITEM import mozilla.components.ui.icons.R as iconsR private val MENU_ITEM_HEIGHT_WITHOUT_DESC = 52.dp @@ -384,6 +392,44 @@ internal fun Badge( } } +@Composable +internal fun ExpandableMenuItemAnimation( + isExpanded: Boolean, + content: @Composable () -> Unit, +) { + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically( + expandFrom = Alignment.Top, + animationSpec = tween( + durationMillis = DURATION_MS_MAIN_MENU_ITEM, + easing = LinearEasing, + ), + ) + fadeIn( + animationSpec = tween( + durationMillis = DURATION_MS_MAIN_MENU_ITEM, + easing = LinearEasing, + ), + ), + exit = shrinkVertically( + shrinkTowards = Alignment.Top, + animationSpec = tween( + durationMillis = DURATION_MS_MAIN_MENU_ITEM, + easing = LinearEasing, + ), + ) + fadeOut( + animationSpec = tween( + durationMillis = DURATION_MS_MAIN_MENU_ITEM, + easing = LinearEasing, + ), + ), + ) { + Column { + content() + } + } +} + /** * Enum containing all the supported state for the menu item. */