diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index b37e9ea223..1786c1139e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -46,6 +46,7 @@ import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -170,14 +171,15 @@ private fun RoomSummaryScaffoldRow( hideAvatarImage: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { - val clickModifier = Modifier.combinedClickable( - onClick = { onClick(room) }, - onLongClick = { onLongClick(room) }, - onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - indication = ripple(), - interactionSource = remember { MutableInteractionSource() } - ) - + val clickModifier = Modifier + .combinedClickable( + onClick = { onClick(room) }, + onLongClick = { onLongClick(room) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + indication = ripple(), + interactionSource = remember { MutableInteractionSource() } + ) + .onShiftF10 { onLongClick(room) } Row( modifier = modifier .fillMaxWidth() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index 00069e9f82..710428bfab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.timeline.model.bubble.BubbleSta import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider import io.element.android.libraries.core.extensions.to01 import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp @@ -96,12 +97,14 @@ fun MessageEventBubble( val clickableModifier = if (isTalkbackActive()) { Modifier } else { - Modifier.combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - indication = ripple(), - interactionSource = interactionSource - ) + Modifier + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + indication = ripple(), + interactionSource = interactionSource + ) + .onShiftF10(onLongClick) } // Ignore state.isHighlighted for now, we need a design decision on it. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt index c988679626..70abb82278 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Surface @@ -46,7 +47,8 @@ fun MessageStateEventContainer( onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), indication = ripple(), interactionSource = interactionSource - ), + ) + .onShiftF10(onLongClick), color = backgroundColor, shape = shape, content = content diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index 8c2a1d9ff6..86005640c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.AggregatedReacti import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions import io.element.android.libraries.designsystem.icons.CompoundDrawables +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp @@ -107,6 +108,7 @@ fun MessagesReactionButton( onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), onLongClick = onLongClick ) + .onShiftF10(onLongClick) // Inner border, to highlight when selected .border(BorderStroke(1.dp, borderColor), RoundedCornerShape(corner = CornerSize(12.dp))) .background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp))) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt index ca34184491..579b825796 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt @@ -34,6 +34,7 @@ import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallStateProvider import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp @@ -57,6 +58,7 @@ internal fun TimelineItemCallNotifyView( onLongClick = { onLongClick(event) }, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), ) + .onShiftF10 { onLongClick(event) } .padding(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 59ec63d842..8753a90c42 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -37,6 +37,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.modifiers.subtleColorStops import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -148,11 +149,13 @@ internal fun TimelineItemRow( // Custom clickable that applies over the whole item for accessibility .then( if (isTalkbackActive()) { - Modifier.combinedClickable( - onClick = { onContentClick(timelineItem) }, - onLongClick = { onLongClick(timelineItem) }, - onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - ) + Modifier + .combinedClickable( + onClick = { onContentClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + .onShiftF10 { onLongClick(timelineItem) } } else { Modifier } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 41d3873a65..94e71afeb2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -48,6 +48,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle @@ -91,10 +92,12 @@ fun TimelineItemImageView( .then(if (isLoaded) Modifier.background(Color.White) else Modifier) .then( if (!isTalkbackActive() && onContentClick != null) { - Modifier.combinedClickable( - onClick = onContentClick, - onLongClick = onLongClick - ) + Modifier + .combinedClickable( + onClick = onContentClick, + onLongClick = onLongClick, + ) + .onShiftF10(onLongClick) } else { Modifier } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index dc7cfb2d52..79e4583ba4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.MediaRequestData @@ -74,6 +75,7 @@ fun TimelineItemStickerView( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), ) + .onShiftF10(onLongClick) } else { Modifier } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 18a59325d5..433d75dbc9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -54,6 +54,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -105,10 +106,12 @@ fun TimelineItemVideoView( .then(if (isLoaded) Modifier.background(Color.White) else Modifier) .then( if (!isTalkbackActive && onContentClick != null) { - Modifier.combinedClickable( - onClick = onContentClick, - onLongClick = onLongClick - ) + Modifier + .combinedClickable( + onClick = onContentClick, + onLongClick = onLongClick, + ) + .onShiftF10(onLongClick) } else { Modifier } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Keyboard.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Keyboard.kt new file mode 100644 index 0000000000..c1a6a57428 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Keyboard.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.modifiers + +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type + +/** + * Modifier to handle Shift + F10 key events. + * This is typically used to trigger context menus in desktop applications. + * + * @param onShiftF10Press The callback to invoke when Shift + F10 is pressed. + */ +fun Modifier.onShiftF10( + onShiftF10Press: (() -> Unit)?, +): Modifier = then( + if (onShiftF10Press == null) { + Modifier + } else { + Modifier.onKeyEvent { keyEvent -> + // invoke the callback when the user presses Shift + F10 + if (keyEvent.type == KeyEventType.KeyUp && + keyEvent.isShiftPressed && + keyEvent.key == Key.F10) { + onShiftF10Press() + true + } else { + false + } + } + } +) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt index c8d735d485..f0410981b8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.core.extensions.withBrackets +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider @@ -84,6 +85,7 @@ private fun FilenameRow( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), ) + .onShiftF10(onLongClick) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt index 1144fc3eaf..bcc19212f4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.core.extensions.withBrackets +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider @@ -84,6 +85,7 @@ private fun FilenameRow( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), ) + .onShiftF10(onLongClick) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt index 917c60df9e..f03f5e0955 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.mediaviewer.impl.model.MediaItem @@ -44,7 +45,8 @@ fun ImageItemView( onClick = onClick, onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - ), + ) + .onShiftF10(onLongClick), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt index cdc66c4eae..6a47567d76 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt @@ -33,6 +33,7 @@ import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -54,7 +55,8 @@ fun VideoItemView( onClick = onClick, onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - ), + ) + .onShiftF10(onLongClick), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt index 5d84b79f8f..032f486cf7 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView +import io.element.android.libraries.designsystem.modifiers.onShiftF10 import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator @@ -105,6 +106,7 @@ private fun VoiceInfoRow( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), ) + .onShiftF10(onLongClick) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically,