From 5323b251aacd2ee41c42776ac4e9d87f67912941 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 21 Aug 2026 14:20:33 +0200 Subject: [PATCH] Answer a right click the way a long press is answered A tablet with a keyboard case is a pointer device, and the way to ask a row for its menu with a pointer is the secondary button: a right click, or a two-finger tap on the touchpad. Nothing in the manager answered it. combinedClickable reads a press from any button as the beginning of an ordinary tap and finds a long press only by timing a held finger, so a right click did whatever a left click does -- opened a module's scope instead of its menu, toggled an app instead of offering to re-optimize it -- and what the hold was for could not be reached at all. The View-based manager never had to say any of this. A View shows its registered context menu on a secondary press by itself, which is why the gesture worked at all before the manager was written in Compose. It worked badly: the menu was built from the row under the pointer but acted on a field that only the long press listener wrote, so it opened against whichever row had been held last, or did nothing when none had been (#644). So the press is caught and handed to the same lambda the long press runs, the two gestures meaning the same thing -- one asked with a finger, one asked with a pointer. It is watched on the initial pass, ahead of the pass foundation listens on, and every event from the moment the button is reported held is consumed, which is also what keeps the click from landing: foundation asks for a press nothing else has taken, and for a release nothing else has taken. The action runs on the way down rather than on the release, which is when a View opens its context menu too. The rows that gain it are the ones a long press already meant something on: a module and its icon in the shared row, an app in a module's scope, and a contributor's avatar and their name on the home screen. Nothing changes for a finger. --- .../org/matrix/vector/ui/ContextClick.kt | 58 +++++++++++++++++++ .../kotlin/org/matrix/vector/ui/ModuleRow.kt | 5 +- .../manager/ui/components/CommitTimeline.kt | 4 +- .../manager/ui/screens/home/HomeScreen.kt | 4 +- .../manager/ui/screens/modules/ScopeScreen.kt | 4 +- 5 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 manager-ui/src/main/kotlin/org/matrix/vector/ui/ContextClick.kt diff --git a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ContextClick.kt b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ContextClick.kt new file mode 100644 index 000000000..8510ea724 --- /dev/null +++ b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ContextClick.kt @@ -0,0 +1,58 @@ +package org.matrix.vector.ui + +import androidx.compose.foundation.combinedClickable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.pointerInput + +/** + * A row's whole gesture set: tap it, hold it — and, from a mouse or a touchpad, right-click it. + * + * A tablet with a keyboard case is a pointer device, and the way to ask a row for its menu with a + * pointer is the secondary button: a right click, or a two-finger tap on the touchpad. Compose has + * no notion of that gesture. `combinedClickable` reads a press from any button as the beginning of + * an ordinary tap and reaches [onLongClick] only by timing a held finger, so on a touchpad every + * row here answered a right click by doing whatever a left click does, and whatever the hold was + * for could not be reached at all. The View-based manager never had to say any of this: a `View` + * opens its registered context menu on a secondary press by itself, which is why the gesture worked + * before the manager was written in Compose. + * + * So the secondary press is caught here and handed to [onLongClick] — the same action, because the + * two gestures mean the same thing, one asked with a finger and one asked with a pointer. + */ +fun Modifier.contextClickable(onClick: () -> Unit, onLongClick: (() -> Unit)? = null): Modifier = + combinedClickable(onClick = onClick, onLongClick = onLongClick) + .then(if (onLongClick == null) Modifier else Modifier.onSecondaryPress(onLongClick)) + +/** + * The secondary button's press, taken before anything underneath can read it as a tap. + * + * On [Initial][PointerEventPass.Initial], the pass that runs before the one `combinedClickable` + * listens on — the same way [org.matrix.vector.ui.navigation.PanelBar] gets ahead of the item it + * wraps — and every event of the press is consumed, which is what keeps the click from landing: + * foundation asks for a press nothing else has taken, and for a release nothing else has taken. + * + * The action runs on the way down rather than on the release, matching the platform: a right click + * on a `View` opens its context menu the moment the button goes down. It runs on the first event + * that reports the button held rather than on the press event alone, because a pointer that + * announces its buttons a moment after it announces the touch would otherwise be missed. + */ +private fun Modifier.onSecondaryPress(action: () -> Unit): Modifier = + pointerInput(action) { + awaitPointerEventScope { + var fired = false + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + if (!event.buttons.isSecondaryPressed) { + fired = false + continue + } + event.changes.forEach { it.consume() } + if (!fired) { + fired = true + action() + } + } + } + } diff --git a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt index 3eb1adb95..ff9d1325e 100644 --- a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt +++ b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -135,7 +134,7 @@ fun ModuleRow( Column( modifier = if (onIconClick != null) - Modifier.combinedClickable(onClick = onIconClick, onLongClick = onIconLongClick) + Modifier.contextClickable(onClick = onIconClick, onLongClick = onIconLongClick) else Modifier, // Against the text, not centred over the badge: the badge below is wider than the icon, // so centring left a gap between the icon and the edge the names all start from. @@ -175,7 +174,7 @@ fun ModuleRow( Modifier.weight(1f) .then( if (onClick != null) - Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick) + Modifier.contextClickable(onClick = onClick, onLongClick = onLongClick) else Modifier ) ) { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/CommitTimeline.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/CommitTimeline.kt index dc711e7c0..8223de2bb 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/CommitTimeline.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/CommitTimeline.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable +import org.matrix.vector.ui.contextClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -212,7 +212,7 @@ fun CommitRow( // means "open this commit", and a long press on a subject line has no obvious // subject; a long press on a name plainly means *that name*. modifier = - Modifier.combinedClickable( + Modifier.contextClickable( onClick = { onOpenCommit(commit) }, onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt index 847eb1d83..b6d00feb4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable +import org.matrix.vector.ui.contextClickable import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.size @@ -922,7 +922,7 @@ private fun ContributorRow( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = - Modifier.combinedClickable( + Modifier.contextClickable( onClick = { if (hasProfile) onClick(person) }, onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt index 4129ae2f0..94606d0a9 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeScreen.kt @@ -43,7 +43,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border -import androidx.compose.foundation.combinedClickable +import org.matrix.vector.ui.contextClickable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack @@ -960,7 +960,7 @@ private fun AppRow( ListItem( modifier = - Modifier.combinedClickable( + Modifier.contextClickable( onClick = { if (enabled) onToggle(!app.isSelectedInScope) }, onLongClick = { // The long press is where re-optimize lives, and re-optimize is the fix