Skip to content

Desktop. Fix input methods on JBR, disable input methods when we lose focus #881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 20, 2023
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 @@ -54,6 +54,11 @@ import org.jetbrains.skiko.*
* Provides a base implementation for integrating a Compose scene with AWT/Swing.
* It allows setting Compose content by [setContent], this content should be drawn on [component].
*
* This bridge contain 2 components that should be added to the view hirarachy:
* [component] the main visible Swing component, on which Compose will be shown
* [invisibleComponent] service component used to bypass Swing issues:
* - for forcing refocus on input methods change
*
* Inheritors should call [attachComposeToComponent], so events that came to [component] will be transferred to [ComposeScene]
*/
internal abstract class ComposeBridge(
Expand All @@ -66,7 +71,10 @@ internal abstract class ComposeBridge(
mainOwnerProvider = { scene.mainOwner }
)

private val _invisibleComponent = InvisibleComponent()

abstract val component: JComponent
val invisibleComponent: Component get() = _invisibleComponent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class ComposeBridge is internal, so no need to add it for field


abstract val renderApi: GraphicsApi

Expand All @@ -86,16 +94,30 @@ internal abstract class ComposeBridge(

private var window: Window? = null

private fun refocus() {
if (component.isFocusOwner) {
_invisibleComponent.requestFocusTemporary()
component.requestFocus()
}
}

private val platformComponent: PlatformComponent = object : PlatformComponent {
override fun enableInput(inputMethodRequests: InputMethodRequests) {
currentInputMethodRequests = inputMethodRequests
component.enableInputMethods(true)
val focusGainedEvent = FocusEvent(focusComponentDelegate, FocusEvent.FOCUS_GAINED)
component.inputContext.dispatchEvent(focusGainedEvent)
// Without resetting the focus, Swing won't update the status (doesn't show/hide popup)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to cover such a case with a test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There 2 useful tests that could be written:

  1. Checking that some Swing component enables input methods when we focus on a textfield. Unfortunately, Swing doesn't provide any API for that.

  2. Checking Chinese input in integration with Chinise input enabled in the system. But for that we need a Docker container, a task on CI, and a special test annotation @environmenttest.

Other kind of tests aren't useful:

  1. Checking that TextField enables Input methods on ComposeScene level. It isn't very needed right now, as a similar test already exists in Jetpack Compose (that checks that we call appropriate PlatformTextInputService methods).
  2. Injecting fake JComponent, on which we check that we call enableInputMethods. But this bounds the test to the implementation details.

// enableInputMethods is design to used per-Swing component level at init stage,
// not dynamically
refocus()
}

override fun disableInput() {
currentInputMethodRequests = null
component.enableInputMethods(false)
// Without resetting the focus, Swing won't update the status (doesn't show/hide popup)
// enableInputMethods is design to used per-Swing component level at init stage,
// not dynamically
refocus()
}

override val locationOnScreen: Point
Expand Down Expand Up @@ -181,6 +203,7 @@ internal abstract class ComposeBridge(

@OptIn(ExperimentalComposeUiApi::class)
protected fun attachComposeToComponent() {
component.enableInputMethods(false)
component.addInputMethodListener(object : InputMethodListener {
override fun caretPositionChanged(event: InputMethodEvent?) {
if (isDisposed) return
Expand Down Expand Up @@ -393,6 +416,12 @@ internal abstract class ComposeBridge(
override val touchSlop: Float get() = with(platformComponent.density) { 18.dp.toPx() }
}
}

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

private fun ComposeScene.onMouseEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
if (bridge != null) {
bridge!!.dispose()
super.remove(bridge!!.component)
super.remove(bridge!!.invisibleComponent)
bridge = null
}
}
Expand Down Expand Up @@ -187,6 +188,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
if (bridge == null) {
bridge = createComposeBridge()
initContent()
super.add(bridge!!.invisibleComponent, Integer.valueOf(1))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add it to another layer then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it make a difference? super.add(, 0) looks the same to me as when we just add it before the other component.

super.add(bridge!!.component, Integer.valueOf(1))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ internal class ComposeWindowDelegate(

init {
layout = null
super.add(bridge.invisibleComponent, 1)
super.add(bridge.component, 1)
}

fun dispose() {
super.remove(bridge.component)
super.remove(bridge.invisibleComponent)
}
}

Expand Down