You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

5.8 KiB

Keyboard events handling

Prerequisites

This tutorial expects that you have already set up the Compose project as described in the Getting Started tutorial

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.

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

It works the same as Compose for Android, for details see API Reference

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:

import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
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.ExperimentalComposeUiApi

@OptIn(ExperimentalComposeUiApi::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.onPreviewKeyEvent {
                    when {
                        (it.isMetaPressed && it.key == Key.Minus) -> {
                            consumedText -= text.length
                            text = ""
                            true
                        }
                        (it.isMetaPressed && it.key == Key.Equals) -> {
                            consumedText += text.length
                            text = ""
                            true
                        }
                        else -> false
                    }
                }
            )
        }
    }
}

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

Window-scoped events

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:

import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
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.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(ExperimentalComposeUiApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
    MaterialTheme {
        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
                        }
                    }
                }.show {
                    Text("I'm popup!")
                }
            }
        ) {
            Text("Open popup")
        }
    }
}

window_keyboard