diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt index 43cb5553..44144e46 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt @@ -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 @@ -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 @@ -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() @@ -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) diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/common/components/ActionInputField.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/common/components/ActionInputField.kt index 71418452..694412a9 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/common/components/ActionInputField.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/common/components/ActionInputField.kt @@ -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 @@ -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 diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/common/keymap/ImeAwareKeyEvent.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/common/keymap/ImeAwareKeyEvent.kt new file mode 100644 index 00000000..05549635 --- /dev/null +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/common/keymap/ImeAwareKeyEvent.kt @@ -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) +}