Contents |
This tutorial explains how to make a QML user interface entirely navigable by keyboard.
If you don't know anything about QML, reading the introductory Qt/QML documentation might be helpful.
Install the MeeGo SDK first. Alternatively, you could just install the meego-sdk-qt-creator and meego-sdk-qt packages (see the same page for the location of the repos).
I'm using Fedora 13 Linux, and have run the application on Fedora 13 and MeeGo 1.1.
You basically need to create a QML application with some input elements to start with.
Then add keyboard navigation using KeyNavigation elements, defining paths between pairs of UI elements (e.g. to move between input elements).
Finally use Keys elements, to connect key presses to actions (e.g. to submit data entered in input elements).
key-nav.qml file, which contains the main UI definition. The first version just displays three input elements:
This is what the QML looks like for these elements:
import Qt 4.7
Rectangle {
id: window
width: 600
height: 300
ListModel {
id: countries
ListElement {
name: "Finland"
}
ListElement {
name: "United Kingdom"
}
ListElement {
name: "USA"
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.top: parent.top
anchors.topMargin: 10
spacing: 20
width: 180
Row {
spacing: 10
width: parent.width
Text {
text: "Name:"
}
TextInput {
id: nameInput
maximumLength: 30
width: maximumLength * 10
focus: true
fillColor: "darkgrey"
}
}
Row {
spacing: 10
height: countries.count * 20
width: parent.width
Text {
text: "Country:"
}
ListView {
id: countrySelector
width: parent.width
height: parent.height
model: countries
highlight: Rectangle {
color: "darkgrey"
radius: 5
}
delegate: Item {
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
height: 20
Text { text: name }
}
}
}
Row {
width: parent.width
height: 20
Rectangle {
id: submitButton
width: parent.width
height: parent.height
color: "darkgrey"
radius: 5
Text {
text: "Submit"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
}
Row {
width: parent.width
height: 20
Text {
id: greeting
text: "the greeting will be here"
}
}
}
}
(I made no attempt at modularisation here, but kept everything in one file to make the explanations simpler. Also, this isn't usable at all yet.)
qmlviewer (big green button, bottom left in Qt Creator). You should see something like this:
Navigating from one QML element to another means that you shift the focus from the currently-focused element to another element. To do this, you add a KeyNavigation property to the element you're navigating from, associated with the arrow key (up, down, left, right) which will trigger the focus shift.
For example, say you had two rectangles, rect1 and rect2. You want a user to be able to navigate from rect1 to rect2 by pressing the right arrow key. To implement this, you would add the following property to rect1:
KeyNavigation.right: rect2
Which means "on pressing the right arrow key, shift the focus to the element with id rect2.
Key navigation is not symmetrical by default, so if you wanted to be able to go back to rect1 from rect2 by pressing the left arrow key, you need to add a property to rect2 as well:
KeyNavigation.left: rect1
Here's a complete example:
import Qt 4.7
Rectangle {
width: 200
height: 100
Rectangle {
id: rect1
focus: true
width: 100
height: 100
opacity: activeFocus ? 1.0 : 0.5
color: "red"
KeyNavigation.right: rect2
}
Rectangle {
id: rect2
width: 100
height: 100
x: 100
opacity: activeFocus ? 1.0 : 0.5
color: "red"
KeyNavigation.left: rect1
}
}
You can navigate from the left-hand red square (rect1) to the right-hand one by pressing the right cursor key; and from the right-hand red square to the left-hand one by pressing the left cursor key. When the focus shifts, the opacity of the focused element is set to 1.0, and the unfocused one to 0.5.
Applying the same principle to our simple UI, we get:
import Qt 4.7
Rectangle {
id: window
width: 600
height: 300
ListModel {
id: countries
ListElement {
name: "Finland"
}
ListElement {
name: "United Kingdom"
}
ListElement {
name: "USA"
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.top: parent.top
anchors.topMargin: 10
spacing: 20
width: 180
Row {
spacing: 10
width: parent.width
opacity: nameInput.activeFocus ? 1.0 : 0.25
Text {
text: "Name:"
}
TextInput {
id: nameInput
maximumLength: 30
width: maximumLength * 10
focus: true
fillColor: "darkgrey"
KeyNavigation.right: countrySelector
}
}
Row {
spacing: 10
height: countries.count * 20
width: parent.width
opacity: countrySelector.activeFocus ? 1.0 : 0.25
Text {
text: "Country:"
}
ListView {
id: countrySelector
width: parent.width
height: parent.height
model: countries
highlight: Rectangle {
color: "darkgrey"
radius: 5
}
delegate: Text {
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
height: 20
text: name
}
KeyNavigation.left: nameInput
KeyNavigation.right: submitButton
}
}
Row {
width: parent.width
height: 20
opacity: submitButton.activeFocus ? 1.0 : 0.25
Rectangle {
id: submitButton
width: parent.width
height: parent.height
color: "darkgrey"
radius: 5
Text {
text: "Submit"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
KeyNavigation.left: countrySelector
}
}
Row {
width: parent.width
height: 20
Text {
id: greeting
text: "the greeting will be here"
}
}
}
}
The differences between this version and the previous one are:
KeyNavigation properties on each of the input elements. Note that I've used left and right arrow keys for navigation, as the up and down arrow keys default to changing the selected item in a ListView, as used for the country selector.
focus property set to true on the element which should receive the initial focus. In this case, it's the name entry field.
Row element, dependent on whether the input element within the row has the activeFocus or not. This fades out any rows which don't contain an element in focus, and highlights the row which currently has the focus. Obviously, other approaches (using color, animations etc.) to highlight the active element are possible.
The Keys element is used to associate key press events on a UI element with some action. In the case of our keyboard-navigable UI, we'll use this to "submit" the data entered in the form and use it to show the user a greeting.
The basic pattern for using Keys is to connect one of the Keys.on* signals to a function. There are signals for each of the common keys (e.g. onReturnPressed, onAsteriskPressed) as well as a generic signal which is emitted whenever any key is pressed on the element (onPressed).
The example below extends the two red rectangles example of the previous section, adding some actions based on key presses.
import Qt 4.7
Rectangle {
width: 200
height: 100
Rectangle {
id: rect1
focus: true
width: 100
height: 100
opacity: activeFocus ? 1.0 : 0.5
color: "red"
KeyNavigation.right: rect2
Keys.onPressed: {
if (event.key != Qt.Key_Right) {
console.log(event.key + " pressed on left rectangle");
event.accepted = true;
}
}
}
Rectangle {
id: rect2
width: 100
height: 100
x: 100
opacity: activeFocus ? 1.0 : 0.5
color: "red"
KeyNavigation.left: rect1
Keys.onReturnPressed: {
console.log("Return pressed on right rectangle");
}
}
}
A couple of things to note:
Keys.onPressed signal handler, we have to make the signal handler discriminate right arrow key presses. Right arrow key presses are effectively ignored, and allowed to propagate to other handlers. Every other key press logs the key value to the console and sets event.accepted = true to stop the event propagating to other handlers.
Keys.onReturnPressed handler which doesn't interfere with the navigation keys and only responds to the Return key (by logging a message to the console). The more-specific key handlers don't have to set event.accepted = true, as this is implicit.
Keys signal is a block of JavaScript enclosed in curly brackets. This code can reference elements declared in the QML script, as we'll see in a moment.
We can now apply the same techniques to our simple UI so that the submit "button" responds to presses of the Return key by displaying a greeting: