diff --git a/tutorials/Keyboard/README.md b/tutorials/Keyboard/README.md index b518aaf3b5..ee94c9abe6 100644 --- a/tutorials/Keyboard/README.md +++ b/tutorials/Keyboard/README.md @@ -8,16 +8,6 @@ This tutorial expects that you have already set up the Compose project as descri In this tutorial, we will look at two different ways of handling keyboard events in Compose for Desktop as well as the utilities that we have to do this. -## KeySets - -Compose for Desktop has a few utilities to work with shortcuts: - -`KeysSet` represents a set of keys that can be simultaneously pressed. You can construct a KeysSet using the Key's extension function: - -``` kotlin -Key.CtrlLeft + Key.Enter -``` - ## Event handlers There are two ways to handle key events in Compose for Desktop: @@ -29,9 +19,7 @@ There are two ways to handle key events in Compose for Desktop: It works the same as Compose for Android, for details see [API Reference](https://developer.android.com/reference/kotlin/androidx/compose/ui/input/key/package-summary#keyinputfilter) -`Modifier.shortcuts` is used to define one or multiple callbacks for `KeysSet`s. - -The most common use case is to define keyboard handlers for active controls like `TextField`. Here is an example: +The most common use case is to define keyboard handlers for active controls like `TextField`. You can use both `onKeyEvent` and `onPreviewKeyEvent` but the last one is usually preferable to define shortcuts while it guarantees you that key events will not be consumed by children components. Here is an example: ```kotlin import androidx.compose.desktop.Window @@ -49,8 +37,9 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue -import androidx.compose.ui.input.key.shortcuts +import androidx.compose.ui.ExperimentalComposeUiApi +@OptIn(ExperimentalComposeUiApi::class) fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { MaterialTheme { var consumedText by remember { mutableStateOf(0) } @@ -58,18 +47,23 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Text("Consumed text: $consumedText") TextField( - value = text, - onValueChange = { text = it }, - modifier = Modifier.shortcuts { - on(Key.CtrlLeft + Key.Minus) { + value = text, + onValueChange = { text = it }, + modifier = Modifier.onPreviewKeyEvent { + when { + (it.isMetaPressed && it.key == Key.Minus) -> { consumedText -= text.length text = "" + true } - on(Key.CtrlLeft + Key.Equals) { + (it.isMetaPressed && it.key == Key.Equals) -> { consumedText += text.length text = "" + true } + else -> false } + } ) } } @@ -77,48 +71,82 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { ``` -Note the annotation `@OptIn(ExperimentalKeyInput::class)`. Keyboard-related event handlers are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. +Note the annotation `@OptIn(ExperimentalKeyInput::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. ![keyInputFilter](keyInputFilter.gif) ## Window-scoped events -`AppWindow` instances have a `keyboard` property. It is possible to use it to define keyboard shortcuts that are always active in the current window. Here is an example: +`LocalAppWindow` instances have a `keyboard` property. It is possible to use it to define keyboard event handlers that are always active in the current window. You also can get window instance for popups. Again, you possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example: ``` kotlin import androidx.compose.desktop.AppWindow +import androidx.compose.desktop.LocalAppWindow import androidx.compose.desktop.Window -import androidx.compose.foundation.Text import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.ExperimentalKeyInput import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue -@OptIn(ExperimentalKeyInput::class) +@OptIn(ExperimentalComposeUiApi::class) fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { MaterialTheme { - Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { - Button( - modifier = Modifier.padding(4.dp), - onClick = { - AppWindow(size = IntSize(200, 200)).also { - it.keyboard.setShortcut(Key.Escape) { - it.close() - } - }.show { - Text("I'm popup!") + var cleared by remember { mutableStateOf(false) } + LocalAppWindow.current.keyboard.onKeyEvent = { + if (it.isMetaPressed && it.isShiftPressed && it.key == Key.C) { + cleared = true + true + } else { + false + } + } + + if (cleared) { + Text("The App was cleared!") + } else { + App() + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun App() { + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { + Button( + modifier = Modifier.padding(4.dp), + onClick = { + AppWindow(size = IntSize(200, 200)).also { window -> + window.keyboard.onPreviewKeyEvent = { + if (it.key == Key.Escape) { + window.close() + true + } else { + false } } - ) { - Text("Open popup") + }.show { + Text("I'm popup!") + } } + ) { + Text("Open popup") } } } diff --git a/tutorials/Keyboard/window_keyboard.gif b/tutorials/Keyboard/window_keyboard.gif index 93d9100723..b6339e7e35 100644 Binary files a/tutorials/Keyboard/window_keyboard.gif and b/tutorials/Keyboard/window_keyboard.gif differ