diff --git a/tutorials/Keyboard/README.md b/tutorials/Keyboard/README.md new file mode 100644 index 0000000000..8e50a849ba --- /dev/null +++ b/tutorials/Keyboard/README.md @@ -0,0 +1,128 @@ +# Keyboard events handling + +## Prerequisites + +This tutorial expects that you have already set up the Compose project as described in the [Getting Started tutorial](../Getting_Started) + +## What is covered + +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: + +- By setting up an event handler based on the element that is in focus +- By setting up an event handler in the scope of the window + +## Focus related events + +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: + +``` kotlin +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.material.MaterialTheme +import androidx.compose.material.TextField +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.* +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 + +@OptIn(ExperimentalKeyInput::class) +fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { + MaterialTheme { + var consumedText by remember { mutableStateOf(0) } + var text by remember { mutableStateOf("") } + 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) { + consumedText -= text.length + text = "" + } + on(Key.CtrlLeft + Key.Equals) { + consumedText += text.length + text = "" + } + } + ) + } + } +} +``` + + +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. + +![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: + +``` kotlin +import androidx.compose.desktop.AppWindow +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.ui.Modifier +import androidx.compose.ui.input.key.ExperimentalKeyInput +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalKeyInput::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!") + } + } + ) { + Text("Open popup") + } + } + } +} +``` + +![window_keyboard](window_keyboard.gif) diff --git a/tutorials/Keyboard/keyInputFilter.gif b/tutorials/Keyboard/keyInputFilter.gif new file mode 100644 index 0000000000..ec7dd773a5 Binary files /dev/null and b/tutorials/Keyboard/keyInputFilter.gif differ diff --git a/tutorials/Keyboard/window_keyboard.gif b/tutorials/Keyboard/window_keyboard.gif new file mode 100644 index 0000000000..93d9100723 Binary files /dev/null and b/tutorials/Keyboard/window_keyboard.gif differ