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