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:
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.
*/