diff --git a/app/src/androidTest/java/org/schabi/newpipe/ui/components/menu/LongPressMenuTest.kt b/app/src/androidTest/java/org/schabi/newpipe/ui/components/menu/LongPressMenuTest.kt index aed3a48d4..f45ed5cc0 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/ui/components/menu/LongPressMenuTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/ui/components/menu/LongPressMenuTest.kt @@ -127,18 +127,18 @@ class LongPressMenuTest { private fun assertEditorIsEnteredAndExitedProperly() { composeRule.onNodeWithContentDescription(R.string.long_press_menu_enabled_actions_description) .assertDoesNotExist() - composeRule.onNodeWithContentDescription(R.string.edit) + composeRule.onNodeWithContentDescription(R.string.long_press_menu_actions_editor) .performClick() composeRule.waitUntil { composeRule.onNodeWithText(R.string.long_press_menu_enabled_actions) .isDisplayed() } - composeRule.onNodeWithContentDescription(R.string.edit) + composeRule.onNodeWithContentDescription(R.string.long_press_menu_actions_editor) .assertDoesNotExist() Espresso.pressBack() composeRule.waitUntil { - composeRule.onNodeWithContentDescription(R.string.edit) + composeRule.onNodeWithContentDescription(R.string.long_press_menu_actions_editor) .isDisplayed() } diff --git a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt index 23a597a96..4053b70a6 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt @@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.Text @@ -26,6 +26,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import org.schabi.newpipe.R +import org.schabi.newpipe.ui.components.common.TooltipIconButton import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.SizeTokens @@ -70,13 +71,12 @@ fun Toolbar( actions = { actions() if (hasSearch) { - IconButton(onClick = { isSearchActive = true }) { - Icon( - painterResource(id = R.drawable.ic_search), - contentDescription = stringResource(id = R.string.search), - tint = MaterialTheme.colorScheme.onSurface - ) - } + TooltipIconButton( + onClick = { isSearchActive = true }, + icon = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search), + tint = MaterialTheme.colorScheme.onSurface + ) } } ) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt index 050f03970..946c98254 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -39,12 +37,11 @@ fun ScaffoldWithToolbar( actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer ), navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } + TooltipIconButton( + onClick = onBackClick, + icon = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) }, actions = actions ) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/Tooltip.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/Tooltip.kt new file mode 100644 index 000000000..7119d3119 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/Tooltip.kt @@ -0,0 +1,84 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.schabi.newpipe.ui.components.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Useful to show a descriptive popup tooltip when something (e.g. a button) is long pressed. This + * happens by default on XML Views buttons, but needs to be done manually in Compose. + * + * @param text the text to show in the tooltip + * @param modifier The [TooltipBox] implementation does not handle modifiers well, since it wraps + * [content] in a [Box], rendering some [content] modifiers useless. Therefore we have to wrap the + * [TooltipBox] in yet another [Box] with its own modifier, passed as a parameter here. + * @param content the content that will show a tooltip when long pressed (e.g. a button) + */ +@Composable +fun SimpleTooltipBox( + text: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Box(modifier = modifier) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(text) } }, + state = rememberTooltipState(), + content = content + ) + } +} + +/** + * An [IconButton] that shows a descriptive popup tooltip when it is long pressed. + * + * @param onClick handles clicks on the button + * @param icon the icon to show inside the button + * @param contentDescription the text to use as content description for the button, + * and also to show in the tooltip + * @param modifier as described in [SimpleTooltipBox] + * @param buttonModifier a modifier for the internal [IconButton] + * @param iconModifier a modifier for the internal [Icon] + * @param tint the color of the icon + */ +@Composable +fun TooltipIconButton( + onClick: () -> Unit, + icon: ImageVector, + contentDescription: String, + modifier: Modifier = Modifier, + buttonModifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + tint: Color = LocalContentColor.current +) { + SimpleTooltipBox( + text = contentDescription, + modifier = modifier + ) { + IconButton( + onClick = onClick, + modifier = buttonModifier + ) { + Icon( + icon, + contentDescription = contentDescription, + tint = tint, + modifier = iconModifier + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt index 4a3c4a57e..967b6b755 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -40,7 +41,6 @@ import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton @@ -70,6 +70,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle @@ -93,6 +94,8 @@ import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorUtil import org.schabi.newpipe.error.UserAction.LONG_PRESS_MENU_ACTION import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.ui.components.common.SimpleTooltipBox +import org.schabi.newpipe.ui.components.common.TooltipIconButton import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails import org.schabi.newpipe.ui.discardAllTouchesIf @@ -284,7 +287,6 @@ private fun LongPressMenuContent( enabled = action.enabled(), modifier = Modifier .height(buttonHeight) - .fillMaxWidth() .weight(1F) ) rowIndex += 1 @@ -355,22 +357,19 @@ fun LongPressMenuDragHandle(onEditActions: () -> Unit) { BottomSheetDefaults.DragHandle( modifier = Modifier.align(Alignment.Center) ) - IconButton( + + // show a small button here, it's not an important button and it shouldn't + // capture the user attention + TooltipIconButton( onClick = onEditActions, - modifier = Modifier.align(Alignment.CenterEnd) - ) { - // show a small button here, it's not an important button and it shouldn't - // capture the user attention - Icon( - imageVector = Icons.Default.Tune, - contentDescription = stringResource(R.string.edit), - // same color and height as the DragHandle - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(2.dp) - .size(16.dp) - ) - } + icon = Icons.Default.Tune, + contentDescription = stringResource(R.string.long_press_menu_actions_editor), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterEnd), + iconModifier = Modifier + .padding(2.dp) + .size(16.dp) + ) } } @@ -519,32 +518,42 @@ fun LongPressMenuHeader( if (subtitle.isNotBlank()) { Spacer(Modifier.height(1.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - inlineContent = getSubtitleInlineContent(), - modifier = if (onUploaderClick == null) { - Modifier + if (onUploaderClick == null) { + LongPressMenuHeaderSubtitle(subtitle) + } else { + val label = if (item.uploader != null) { + stringResource(R.string.show_channel_details_for, item.uploader) } else { - Modifier.clickable( - onClick = onUploaderClick, - onClickLabel = if (item.uploader != null) { - stringResource(R.string.show_channel_details_for, item.uploader) - } else { - stringResource(R.string.show_channel_details) - } + stringResource(R.string.show_channel_details) + } + SimpleTooltipBox( + text = label + ) { + LongPressMenuHeaderSubtitle( + subtitle, + Modifier.clickable(onClick = onUploaderClick, onClickLabel = label) ) } - .fillMaxWidth() - .fadedMarquee(edgeWidth = 12.dp) - .testTag("ShowChannelDetails") - ) + } } } } } } +@Composable +private fun LongPressMenuHeaderSubtitle(subtitle: AnnotatedString, modifier: Modifier = Modifier) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + inlineContent = getSubtitleInlineContent(), + modifier = modifier + .fillMaxWidth() + .fadedMarquee(edgeWidth = 12.dp) + .testTag("ShowChannelDetails") + ) +} + fun getSubtitleAnnotatedString( item: LongPressable, showLink: Boolean, @@ -618,30 +627,33 @@ fun LongPressMenuButton( icon: ImageVector, text: String, onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true + enabled: Boolean, + modifier: Modifier = Modifier ) { - // TODO possibly make it so that when you long-press on the button, the label appears on-screen - // as a small popup, so in case the label text is cut off the users can still read it in full - OutlinedButton( - onClick = onClick, - enabled = enabled, - shape = MaterialTheme.shapes.large, - contentPadding = PaddingValues(start = 3.dp, top = 8.dp, end = 3.dp, bottom = 2.dp), - border = null, + SimpleTooltipBox( + text = text, modifier = modifier ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(32.dp) - ) - FixedHeightCenteredText( - text = text, - lines = 2, - style = MaterialTheme.typography.bodySmall - ) + OutlinedButton( + onClick = onClick, + enabled = enabled, + shape = MaterialTheme.shapes.large, + contentPadding = PaddingValues(start = 3.dp, top = 8.dp, end = 3.dp, bottom = 2.dp), + border = null, + modifier = Modifier.fillMaxSize() + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(32.dp) + ) + FixedHeightCenteredText( + text = text, + lines = 2, + style = MaterialTheme.typography.bodySmall + ) + } } } } @@ -659,6 +671,7 @@ private fun LongPressMenuButtonPreviews() { icon = entry.icon, text = stringResource(entry.label), onClick = { }, + enabled = true, modifier = Modifier.size(86.dp) ) } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt index c790e8d80..6d755984e 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt @@ -42,7 +42,6 @@ import androidx.compose.material.icons.filled.RestartAlt import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -76,6 +75,7 @@ import kotlin.math.floor import org.schabi.newpipe.R import org.schabi.newpipe.ktx.letIf import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar +import org.schabi.newpipe.ui.components.common.TooltipIconButton import org.schabi.newpipe.ui.detectDragGestures import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.text.FixedHeightCenteredText @@ -199,12 +199,11 @@ private fun ResetToDefaultsButton(onClick: () -> Unit) { ) } - IconButton(onClick = { showDialog = true }) { - Icon( - imageVector = Icons.Default.RestartAlt, - contentDescription = stringResource(R.string.playback_reset) - ) - } + TooltipIconButton( + onClick = { showDialog = true }, + icon = Icons.Default.RestartAlt, + contentDescription = stringResource(R.string.playback_reset) + ) } @Composable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e68cd351f..5c6a394fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -872,7 +872,6 @@ SoundCloud has discontinued the original Top 50 charts. The corresponding tab has been removed from your main page. Next NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently. - Edit %d comment %d comments