The Problem
A desktop shell lives and dies by keyboard shortcuts. Super+Space opens the launcher. Super+1–9 switches workspaces. Escape closes a popup. Tab moves focus between controls. Unlike a web app where the browser manages focus automatically, a QML desktop shell must explicitly handle keyboard input and focus.
The Naive Approach
You could add a MouseArea to everything and bind keyboard actions to specific objects:
Item {
focus: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) close()
if (event.key === Qt.Key_Space && event.modifiers & Qt.MetaModifier) openLauncher()
}
}But when multiple objects compete for keyboard input, who gets the key press? The answer depends on focus.
Focus is like a spotlight. Only the object in the spotlight receives keyboard events. You can move the spotlight with Tab, click, or programmatically. The Keys attached property lets an object declare what it does when it's in the spotlight.
The Idea
In QML, focus flows through the object tree:
focus: true— an item requests focus. Only one item per window can have focus at a time.Keys.onPressed— attached property that handles key events when the item has focus.KeyNavigation— automatic focus movement with Tab/arrow keys.Shortcut— global keyboard shortcuts that work regardless of focus.
Let's Build It
import QtQuick
import QtQuick.Window
Window {
width: 500
height: 300
visible: true
title: "Keys & Focus"
// Root item with keyboard handling
Item {
anchors.fill: parent
focus: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
statusText = "Escape pressed — root handler"
event.accepted = true
}
if (event.key === Qt.Key_F1) {
statusText = "F1 — show help"
event.accepted = true
}
}
}
// Search bar
Rectangle {
id: searchBar
x: 20; y: 20
width: 460; height: 44
radius: 8
color: searchInput.focus ? "white" : "#f5f5f5"
border.color: searchInput.focus ? "#1a56db" : "#ddd"
border.width: 2
Behavior on border.color {
ColorAnimation { duration: 100 }
}
TextInput {
id: searchInput
anchors {
left: parent.left; right: parent.right
verticalCenter: parent.verticalCenter
leftMargin: 12; rightMargin: 12
}
font.pixelSize: 16
color: "#333"
placeholderText: "Search apps..."
placeholderTextColor: "#aaa"
// Own keyboard handler
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
statusText = "Searching: " + text
event.accepted = true
}
if (event.key === Qt.Key_Escape) {
focus = false // Release focus to parent
event.accepted = true
}
}
}
// Click to focus
MouseArea {
anchors.fill: parent
onClicked: searchInput.forceActiveFocus()
}
}
// Focusable items
Row {
x: 20; y: 90
spacing: 8
Repeater {
model: ["App 1", "App 2", "App 3", "App 4", "App 5"]
delegate: Rectangle {
required property string modelData
required property int index
width: 80; height: 60; radius: 8
color: itemFocus.containsFocus ? "#1a56db" :
itemMouse.containsMouse ? "#4ecdc4" : "#e0e0e8"
focus: index === 0 // First item starts focused
Text {
text: modelData
anchors.centerIn: parent
color: itemFocus.containsFocus ? "white" : "#333"
font.pixelSize: 13
font.bold: itemFocus.containsFocus
}
// Focus handling
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
statusText = "Activated: " + modelData
event.accepted = true
}
}
// Tab navigation between items
KeyNavigation.right: index < 4 ? itemAt(index + 1) : null
KeyNavigation.left: index > 0 ? itemAt(index - 1) : null
MouseArea {
id: itemMouse
anchors.fill: parent
hoverEnabled: true
onClicked: parent.focus = true
}
// Visual focus indicator
Rectangle {
id: itemFocus
anchors.fill: parent
color: "transparent"
border.color: "#1a56db"
border.width: 2
radius: 8
visible: parent.focus
}
}
}
}
// Function to get item by index
function itemAt(index) {
return appRow.children[index]
}
property Item appRow: appRow
Row {
id: appRow
x: 20; y: 90
spacing: 8
}
// Status
Text {
id: statusText
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
bottomMargin: 12
leftMargin: 20
}
text: "Tab between items, Enter to select, Esc to clear"
color: "#888"
font.pixelSize: 12
}
}Focus patterns:
TextInput— automatically requests focus on click.forceActiveFocus()programmatically gives it focus.Keys.onPressed— attached to any Item. Receives key events when that item has focus.KeyNavigation.right/.left— declare which item gets focus when Tab or arrows are pressed.parent.focus = true— clicking aMouseAreaassigns focus to its parent.event.accepted = true— prevents the event from propagating to parent handlers.
Global Shortcuts
For shell-wide keyboard shortcuts (regardless of focus), use Shortcut:
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Shortcut {
sequences: ["Meta+Space", "Alt+Space"]
onActivated: launcher.toggle()
}Shortcut from QtQuick.Controls works regardless of which item has focus.
Multiple items with focus: true. Only one item per window can have focus. Setting focus: true on two items results in the last one winning. Use forceActiveFocus() to explicitly set focus at runtime.
Not accepting keyboard events. If you handle a key press but don't set event.accepted = true, the event propagates to parent items and may trigger unintended handlers.
Forgetting KeyNavigation for list navigation. Without KeyNavigation, Tab cycles through all focusable items in declaration order, which may not match your visual layout. Use KeyNavigation to create logical Tab/arrow flow.
Under the Hood
Focus is managed by QQuickWindow. When a key event arrives from the windowing system, Qt Quick delivers it to the item with activeFocus. If that item doesn't handle the event (or sets event.accepted = false), the event propagates up the parent chain.
KeyNavigation is implemented as a focus-forwarding mechanism: when a key matching a KeyNavigation direction is pressed, the source item surrenders focus to the target item.
Exercises
- Only the focused item receives keyboard events via Keys.onPressed
- KeyNavigation enables Tab/arrow-based focus movement
- Shortcut creates global keyboard shortcuts independent of focus
- Use event.accepted to prevent event propagation to parent handlers