Add tooltips for long press menu icons

This commit is contained in:
Stypox 2026-02-10 11:37:46 +01:00
parent 3aa5edc453
commit 3fc4bc9cd3
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
7 changed files with 174 additions and 82 deletions

View File

@ -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()
}

View File

@ -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
)
}
}
)

View File

@ -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
)

View File

@ -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
)
}
}
}

View File

@ -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)
)
}

View File

@ -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

View File

@ -872,7 +872,6 @@
<string name="migration_info_6_7_message">SoundCloud has discontinued the original Top 50 charts. The corresponding tab has been removed from your main page.</string>
<string name="auto_queue_description">Next</string>
<string name="newpipe_extractor_description">NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.</string>
<string name="edit">Edit</string>
<plurals name="comments">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>