https://kotlinlang.org logo
#compose-desktop
Title
# compose-desktop
s

Sebastian Lehrbaum

11/28/2023, 3:46 PM
In my compose desktop application, I want the user to start voice input with the space key. However I obviously don't want that to happen if the user is in a textfield, as they may be using the space key as part of their text input. I expected text fields to consume the key event, however I still get the event on the window even when a textfield is selected (it also adds the space in the texfield). Any idea how to avoid it?
Copy code
Window(
    ...
	onKeyEvent = { keyEvent ->
		if (keyEvent.key == Key.Spacebar && keyEvent.type == KeyEventType.KeyDown) {
			println("Got space") // happens even when a textfield consumes the key event first
			true
		} else {
			false
		}
	}
)
s

SaurabhS

11/28/2023, 4:18 PM
Try consuming the
KeyDown
event. I fixed a similar problem yesterday caused by consuming only the
KeyUp
event.
s

Sebastian Lehrbaum

11/28/2023, 5:07 PM
That did not change anything sadly. Except I get multiple events if I hold it down. You are however correct that the textfield should be consuming the KeyDown event, so I fixed my code.
h

Hamba

11/28/2023, 5:18 PM
maybe you could create a wrapper composable that has a textfield with onFocusChanged modifier to run a function that sets a variable (in the view model i guess), and then in your keyevent check that variable is false.
Copy code
Modifier.onFocusChanged { myFlag = it.isFocused }
s

Sebastian Lehrbaum

11/28/2023, 5:23 PM
@Hamba The app has multiple screens and should be able to support the voice commands from almost all of them, that is why the listener is attached to the window. Having to adjust every single TextView, which are also non trivial, is a solution I would very much want to avoid.
a

Alexander Maryanovsky

11/28/2023, 6:31 PM
If the textfield doesn’t consume it you don’t have much choice I’m afraid.
s

Sebastian Lehrbaum

11/28/2023, 6:32 PM
@Alexander Maryanovsky I see. Is this considered expected behaviour or worth a bug report?
a

Alexander Maryanovsky

11/28/2023, 6:36 PM
Hard to say. We get the textfield implementation from Jetpack Compose, and it’s not obvious whether it should consume them. You could ask in the regular compose channel, or submit a bug on Google’s bugtracker.
z

Zach Klippenstein (he/him) [MOD]

11/28/2023, 7:04 PM
This sounds like a bug, right @Grant Toepfer? Text field should consume the events that result in input
a

Alexander Maryanovsky

11/28/2023, 7:05 PM
Or wait for Zach to pop in 😄
s

Sebastian Lehrbaum

11/28/2023, 7:06 PM
I will try to verify what happens on Android, afaik it supports hardware keyboards so it should behave similar. Will be back
g

Grant Toepfer

11/28/2023, 7:14 PM
I recall last time I looked at btf1 onKeyEvent that keys consumed by the IME had different behaviors. I want to say it skipped the onKeyEvent but I'd have to double check (not at my desk rn)
s

Sebastian Lehrbaum

11/28/2023, 7:59 PM
@Alexander Maryanovsky The issue does not appear on Android. Since there is no window on Android, I tried both overwriting onKeyDown in the Activity and adding an
onKeyEvent
to the top most surface. In both cases, once the TextField was selected, no keyDown events were passed to the listeners.
a

Alexander Maryanovsky

11/28/2023, 8:09 PM
Looks like on Android, the KEY_PRESSED (but not KEY_RELEASED) is consumed. On desktop neither is consumed.
1
Ok, so what happens is that
TextFieldKeyInput.process
consumes events that are
KeyEvent.isTypedEvent
.
On Android, this corresponds to
android.view.KeyEvent.ACTION_DOWN
On desktop, this corresponds to
java.awt.event.KeyEvent.KEY_TYPED
So what you want to do is to listen to the events that it consumes
The problem is that
KeyEvent.isTypedEvent
is internal
s

Sebastian Lehrbaum

11/28/2023, 8:20 PM
You sure it's internal? I can access this function in TextFieldKeyInput.desktop.kt
Copy code
actual val KeyEvent.isTypedEvent: Boolean
    get() = awtEventOrNull?.id == java.awt.event.KeyEvent.KEY_TYPED &&
        awtEventOrNull?.keyChar?.isPrintable() == true
It also seems to be filtering correctly. It doesn't have a keyCode so it still needs some fixing, but that seems to be a way to go, thank you so much!
I see what you mean, the expect function is internal, the actual function is not. And since this is Desktop specific code, I can actually use the actual function.
a

Alexander Maryanovsky

11/28/2023, 8:22 PM
Ah, great then
Just check
event.isTypedEvent && event.key == <http://Key.SPACE|Key.SPACE>
Key.Spacebar
rather
s

Sebastian Lehrbaum

11/28/2023, 8:32 PM
keyEvent.key
gives
Key: Unknown keyCode: 0x0
. So does
(keyEvent.nativeKeyEvent as java.awt.event.KeyEvent).keyCode
It appears that the events KEY_TYPED are rather special. What does work is
(keyEvent.nativeKeyEvent as java.awt.event.KeyEvent).extendedKeyCode == java.awt.event.KeyEvent.VK_SPACE
At least for now. Do you have any insight into whether that will stay this way? I'm not sure how stable this part is considered.
a

Alexander Maryanovsky

11/28/2023, 8:34 PM
awtEventOrNull?.keyChar
doesn’t help?
s

Sebastian Lehrbaum

11/28/2023, 8:36 PM
keyEvent.awtEventOrNull?.keyChar == ' '
Works indeed, thanks. That is much nicer to read
I have created a PR against the documentation to mention this behaviour. Let me know if I should fix anything. I think this behaviour is worth documenting. https://github.com/JetBrains/compose-multiplatform/pull/3988
z

Zach Klippenstein (he/him) [MOD]

11/29/2023, 1:40 AM
I would think fields on desktop should consume the other events too? But that’s up to JB
a

Alexander Maryanovsky

11/29/2023, 4:54 AM
I agree, but I think it’s actually up to Google 😄 The textfield code reacts (and consumes) to those events that are isTypedEvent. We just implement the actual for that. On Android the ACTION_UP remains unconsumed. On the desktop, KEY_PRESSED and KEY_RELEASED remain unconsumed because in AWT there’s a 3rd type of event (KEY_TYPED).
If we implement isTypedEvent as both KEY_PRESSED and KEY_TYPED, in order to behave like Android (leaving only KEY_RELEASED unconsumed), you’d have two characters added per physical key press.
Another option is to just completely filter out KEY_TYPED events at a lower level. But that’s a decision about key event dispatching in general, not related to textfields.
z

Zach Klippenstein (he/him) [MOD]

11/29/2023, 7:33 PM
Ideally, the current design would have been reviewed by JB folks to avoid this problem in the first place. I think we generally could be a lot better at including JB reviewers on compose design docs.
a

Alexander Maryanovsky

11/29/2023, 7:36 PM
I imagine it was at some point, but that was a while back, and before I joined. Perhaps this issue didn’t come up; it does sound like something that could be easily missed.
s

Sebastian Lehrbaum

11/29/2023, 8:23 PM
I did a little more reading into it, according to documentation, the KEY_PRESSED and KEY_RELEASED events do not contain the key character, only the key code. While key code depends on the button on the keyboard, the key character depends on your selected layout. So when I switch language for my keyboard, I get the same keyCode but different key characters. Therefore to get the correct character for the input field, one needs to use the KEY_TYPED event. https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyEvent.html#getKeyChar-- Interestingly, in practice, my KEY_PRESSED and KEY_RELEASED events still contained the character, but that is not expectable.
Copy code
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_PRESSED,keyCode=59,keyText=;,keyChar='ö',keyLocation=KEY_LOCATION_STANDARD,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x10000d6] on canvas0)
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_TYPED,keyCode=0,keyText=Unknown keyCode: 0x0,keyChar='ö',keyLocation=KEY_LOCATION_UNKNOWN,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x10000d6] on canvas0)
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_RELEASED,keyCode=59,keyText=;,keyChar='ö',keyLocation=KEY_LOCATION_STANDARD,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x10000d6] on canvas0)
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_PRESSED,keyCode=59,keyText=;,keyChar=';',keyLocation=KEY_LOCATION_STANDARD,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x3b] on canvas0)
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_TYPED,keyCode=0,keyText=Unknown keyCode: 0x0,keyChar=';',keyLocation=KEY_LOCATION_UNKNOWN,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x3b] on canvas0)
Got keyEvent KeyEvent(nativeKeyEvent=java.awt.event.KeyEvent[KEY_RELEASED,keyCode=59,keyText=;,keyChar=';',keyLocation=KEY_LOCATION_STANDARD,rawCode=0,primaryLevelUnicode=0,scancode=0,extendedKeyCode=0x3b] on canvas0)
I think the best short-term solution is to correctly document this behaviour
39 Views