Skip to content

Commit

Permalink
Improve support for BasicTextField2 on Desktop (#1496)
Browse files Browse the repository at this point in the history
- Add support for input methods.
- Replace deprecated `PlatformTextInputService` with
`PlatformContextTextInputService` (name suggestions welcome) in public
APIs.

Fixes (mostly?)
https://youtrack.jetbrains.com/issue/CMP-1253/Support-BasicTextField2-for-desktop
Fixes
https://youtrack.jetbrains.com/issue/CMP-5913/Input-method-cannot-switch-to-Chinses-in-BasicTextField-when-using-TextFieldState

## Testing
- `WindowTypeTest` now tests the new `BasicTextField2`, in addition to
the old `TextField`.

This should be tested by QA

## Release Notes
### Features - Desktop
- Added support for input methods (languages such as Chinese, Korean,
Arabic) to BasicTextField(TextFieldState, ...).

---------

Co-authored-by: Igor Demin <igordmn@gmail.com>
  • Loading branch information
m-sasha and igordmn authored Aug 30, 2024
1 parent 00c3a57 commit 588452e
Show file tree
Hide file tree
Showing 16 changed files with 460 additions and 188 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Android Open Source Project
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,14 +17,17 @@
package androidx.compose.foundation.text.input.internal

import androidx.compose.foundation.content.internal.ReceiveContentConfiguration
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.PlatformTextInputSession
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.EditProcessor
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import kotlinx.coroutines.awaitCancellation
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.coroutines.flow.MutableSharedFlow

// TODO(https://youtrack.jetbrains.com/issue/COMPOSE-733/Merge-1.6.-Apply-changes-for-the-new-text-input) implement
internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession(
state: TransformedTextFieldState,
layoutState: TextLayoutState,
Expand All @@ -34,5 +37,54 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe
stylusHandwritingTrigger: MutableSharedFlow<Unit>?,
viewConfiguration: ViewConfiguration?
): Nothing {
awaitCancellation()
}
val editProcessor = EditProcessor()
fun onEditCommand(commands: List<EditCommand>) {
editProcessor.reset(
value = with(state.visualText) {
TextFieldValue(
text = toString(),
selection = selection,
composition = composition
)
},
textInputSession = null
)

val newValue = editProcessor.apply(commands)

state.replaceAll(newValue.text)
state.editUntransformedTextAsUser {
val untransformedSelection = state.mapFromTransformed(newValue.selection)
setSelection(untransformedSelection.start, untransformedSelection.end)

val composition = newValue.composition
if (composition == null) {
commitComposition()
} else {
val untransformedComposition = state.mapFromTransformed(composition)
setComposition(untransformedComposition.start, untransformedComposition.end)
}
}
}

startInputMethod(
SkikoPlatformTextInputMethodRequest(
state = TextFieldValue(
state.visualText.toString(),
state.visualText.selection,
state.visualText.composition,
),
imeOptions = imeOptions,
onEditCommand = ::onEditCommand,
onImeAction = onImeAction
)
)
}

@OptIn(ExperimentalComposeUiApi::class)
private data class SkikoPlatformTextInputMethodRequest(
override val state: TextFieldValue,
override val imeOptions: ImeOptions,
override val onEditCommand: (List<EditCommand>) -> Unit,
override val onImeAction: ((ImeAction) -> Unit)?
): PlatformTextInputMethodRequest
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ class SkikoComposeUiTest @InternalTestApi constructor(
private inner class TestContext : PlatformContext by PlatformContext.Empty {
override val windowInfo: WindowInfo = TestWindowInfo()

override val textInputService: PlatformTextInputService = TestTextInputService()
override val textInputService = TestTextInputService()

override val rootForTestListener: PlatformContext.RootForTestListener
get() = composeRootRegistry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import androidx.compose.ui.text.TextLayoutResult
* close it with [stopInput].
*/
// Open for testing purposes.
@Deprecated("Use PlatformTextInputModifierNode instead.")
open class TextInputService(private val platformTextInputService: PlatformTextInputService) {
private val _currentInputSession: AtomicReference<TextInputSession?> =
AtomicReference(null)
Expand Down Expand Up @@ -294,7 +293,6 @@ class TextInputSession(
/**
* Platform specific text input service.
*/
@Deprecated("Use PlatformTextInputModifierNode instead.")
interface PlatformTextInputService {
/**
* Start text input session for given client.
Expand Down
6 changes: 6 additions & 0 deletions compose/ui/ui/api/desktop/ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -3299,6 +3299,8 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext {
public fun requestFocus ()Z
public fun setPointerIcon (Landroidx/compose/ui/input/pointer/PointerIcon;)V
public fun startDrag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z
public fun textInputSession (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun textInputSession$suspendImpl (Landroidx/compose/ui/platform/PlatformContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class androidx/compose/ui/platform/PlatformContext$Companion {
Expand Down Expand Up @@ -3360,6 +3362,10 @@ public abstract interface class androidx/compose/ui/platform/PlatformTextInputIn
}

public abstract interface class androidx/compose/ui/platform/PlatformTextInputMethodRequest {
public abstract fun getImeOptions ()Landroidx/compose/ui/text/input/ImeOptions;
public abstract fun getOnEditCommand ()Lkotlin/jvm/functions/Function1;
public abstract fun getOnImeAction ()Lkotlin/jvm/functions/Function1;
public abstract fun getState ()Landroidx/compose/ui/text/input/TextFieldValue;
}

public abstract interface class androidx/compose/ui/platform/PlatformTextInputModifierNode : androidx/compose/ui/node/DelegatableNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal class DesktopTextInputService(private val component: PlatformComponent)
data class CurrentInput(
var value: TextFieldValue,
val onEditCommand: ((List<EditCommand>) -> Unit),
val onImeActionPerformed: ((ImeAction) -> Unit),
val onImeActionPerformed: (ImeAction) -> Unit,
val imeAction: ImeAction,
var focusedRect: Rect? = null
)
Expand All @@ -64,7 +64,10 @@ internal class DesktopTextInputService(private val component: PlatformComponent)
onImeActionPerformed: (ImeAction) -> Unit
) {
val input = CurrentInput(
value, onEditCommand, onImeActionPerformed, imeOptions.imeAction
value = value,
onEditCommand = onEditCommand,
onImeActionPerformed = onImeActionPerformed,
imeAction = imeOptions.imeAction
)
currentInput = input

Expand All @@ -83,14 +86,12 @@ internal class DesktopTextInputService(private val component: PlatformComponent)
}

override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
currentInput?.let { input ->
input.value = newValue
}
currentInput?.value = newValue
}

// TODO(https://github.com/JetBrains/compose-jb/issues/2040): probably the position of input method
// popup isn't correct now
@Deprecated("This method should not be called, used BringIntoViewRequester instead.")
@Deprecated("This method should not be called, use BringIntoViewRequester instead.")
override fun notifyFocusedRect(rect: Rect) {
currentInput?.let { input ->
input.focusedRect = rect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.SessionMutex
import androidx.compose.ui.awt.AwtEventListener
import androidx.compose.ui.awt.AwtEventListeners
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
Expand Down Expand Up @@ -48,14 +50,15 @@ import androidx.compose.ui.platform.DesktopTextInputService
import androidx.compose.ui.platform.EmptyViewConfiguration
import androidx.compose.ui.platform.PlatformComponent
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.PlatformTextInputSessionScope
import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.a11y.AccessibilityController
import androidx.compose.ui.platform.a11y.ComposeSceneAccessible
import androidx.compose.ui.scene.skia.SkiaLayerComponent
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
Expand Down Expand Up @@ -90,6 +93,8 @@ import javax.swing.JComponent
import javax.swing.SwingUtilities
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.ClipRectangle
import org.jetbrains.skiko.ExperimentalSkikoApi
Expand Down Expand Up @@ -682,7 +687,18 @@ internal class ComposeSceneMediator(

override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds
override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration()
override val textInputService: PlatformTextInputService = this@ComposeSceneMediator.textInputService
override val textInputService = this@ComposeSceneMediator.textInputService

private val textInputSessionMutex = SessionMutex<DesktopTextInputSession>()

override suspend fun textInputSession(
session: suspend PlatformTextInputSessionScope.() -> Nothing
): Nothing = textInputSessionMutex.withSessionCancellingPrevious(
sessionInitializer = {
DesktopTextInputSession(coroutineScope = it)
},
session = session
)

override fun setPointerIcon(pointerIcon: PointerIcon) {
contentComponent.cursor =
Expand Down Expand Up @@ -740,6 +756,35 @@ internal class ComposeSceneMediator(
get() = contentComponent.density
}

@OptIn(InternalComposeUiApi::class)
private inner class DesktopTextInputSession(
coroutineScope: CoroutineScope,
) : PlatformTextInputSessionScope, CoroutineScope by coroutineScope {

private val innerSessionMutex = SessionMutex<Nothing?>()

override suspend fun startInputMethod(
request: PlatformTextInputMethodRequest
): Nothing = innerSessionMutex.withSessionCancellingPrevious(
// This session has no data, just init/dispose tasks.
sessionInitializer = { null }
) {
(suspendCancellableCoroutine<Nothing> { continuation ->
textInputService.startInput(
value = request.state,
imeOptions = request.imeOptions,
onEditCommand = request.onEditCommand,
onImeActionPerformed = request.onImeAction ?: {}
)

continuation.invokeOnCancellation {
textInputService.stopInput()
}
})
}
}


private class InvisibleComponent : Component() {
fun requestFocusTemporary(): Boolean {
return super.requestFocus(true)
Expand Down
Loading

0 comments on commit 588452e

Please sign in to comment.