Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
Expand Down Expand Up @@ -120,6 +119,7 @@ import io.askimo.core.util.TimeUtil
import io.askimo.core.util.formatFileSize
import io.askimo.ui.common.i18n.stringResource
import io.askimo.ui.common.keymap.KeyMapManager
import io.askimo.ui.common.keymap.onImeAwarePreviewKeyEvent
import io.askimo.ui.common.theme.AppComponents
import io.askimo.ui.common.theme.AppComponents.dropdownMenu
import io.askimo.ui.common.theme.AppTextStyles
Expand Down Expand Up @@ -477,9 +477,8 @@ fun chatInputField(
Column(
modifier = modifier
.fillMaxWidth()
.onPreviewKeyEvent { keyEvent ->
val shortcut = KeyMapManager.handleKeyEvent(keyEvent)
when (shortcut) {
.onImeAwarePreviewKeyEvent(inputText.composition) { keyEvent ->
when (KeyMapManager.handleKeyEvent(keyEvent)) {
KeyMapManager.AppShortcut.ATTACH_FILE -> {
if (!isLoading) {
openFileDialog()
Expand Down Expand Up @@ -642,9 +641,8 @@ fun chatInputField(
.onGloballyPositioned { coordinates ->
textFieldWidthPx = coordinates.size.width.toFloat()
}
.onPreviewKeyEvent { keyEvent ->
val shortcut = KeyMapManager.handleKeyEvent(keyEvent)
when (shortcut) {
.onImeAwarePreviewKeyEvent(inputText.composition) { keyEvent ->
when (KeyMapManager.handleKeyEvent(keyEvent)) {
KeyMapManager.AppShortcut.NEW_LINE -> {
val cursorPosition = inputText.selection.start
val textBeforeCursor = inputText.text.substring(0, cursorPosition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import io.askimo.ui.common.keymap.KeyMapManager
import io.askimo.ui.common.keymap.onImeAwarePreviewKeyEvent
import io.askimo.ui.common.theme.AppComponents
import io.askimo.ui.common.theme.AppTextStyles
import io.askimo.ui.common.theme.Spacing
Expand Down Expand Up @@ -80,7 +80,7 @@ internal fun actionInputField(
) {
val canSend = enabled && !isLoading && value.text.isNotBlank()

val keyModifier = Modifier.onPreviewKeyEvent { keyEvent ->
val keyModifier = Modifier.onImeAwarePreviewKeyEvent(value.composition) { keyEvent ->
when (KeyMapManager.handleKeyEvent(keyEvent)) {
KeyMapManager.AppShortcut.NEW_LINE -> {
val cursor = value.selection.start
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* SPDX-License-Identifier: AGPLv3
*
* Copyright (c) 2026 Askimo
*/
package io.askimo.ui.common.keymap

import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.text.TextRange

/**
* A drop-in replacement for [onPreviewKeyEvent] that automatically suppresses shortcut
* interception during active IME composition sessions (Korean, Japanese, Chinese, etc.).
*
* ## Why this matters
* On Compose Multiplatform desktop, [onPreviewKeyEvent] fires for **every** AWT key event,
* including those that occur while the IME is mid-composition (e.g. assembling a Korean
* syllable). If a shortcut handler consumes Space or Enter at that point, the IME never
* receives the event and the composed character is dropped or joined to the next word.
*
* ## Usage
* Replace every `.onPreviewKeyEvent { ... }` on a text field (or its ancestor) with:
* ```kotlin
* .onImeAwarePreviewKeyEvent(textFieldValue.composition) { keyEvent -> ... }
* ```
*
* @param composition Pass [androidx.compose.ui.text.input.TextFieldValue.composition].
* When non-null the IME has an active composition in progress and all key events are
* passed through unconsumed to let the IME handle them natively.
* @param onKeyEvent The shortcut handler — identical contract to [onPreviewKeyEvent].
* Return `true` to consume the event, `false` to let it propagate.
*/
fun Modifier.onImeAwarePreviewKeyEvent(
composition: TextRange?,
onKeyEvent: (KeyEvent) -> Boolean,
): Modifier = onPreviewKeyEvent { keyEvent ->
if (composition != null) return@onPreviewKeyEvent false
onKeyEvent(keyEvent)
}