Module 3: Designing your Ubuntu Touch app with QML

In the previous module you learned the basics of a QML app. In this module we’re going to cover a couple of other useful QML components to extend our shopping list app.

You can find this module’s code of the app in the GitLab repository of the course.

1. Introduction

QML has a lot of components that all have their own properties and behavior, more than we can explain in this introductory course. That’s why in this module we’ll show you how to find more information about what you can do with these QML components, so you can explore yourself what properties they can have and what their behavior is. We’ll also give some tips to give your app a nice design.

Starting from the QML Only template created by Clickable, you learned about the major QML components of an Ubuntu Touch app, such as MainView, Page, PageHeader, Label, Button, TextField, ListModel, ListView, ListItem, and ListElement. In this module we’re also going to add a couple of others to spice up your shopping list app.

2. Using the system theme in your app

Let’s first take a step back. In the previous module you learned how to set the foreground and background colors of various QML components. However, in general it’s better to not set any colors explicitly in your app, and just let your app use your system’s default theme. This makes your app blend in more with the operating system and other apps.

The default theme of Ubuntu Touch is Ambiance, with light grey backgrounds. This is also the theme used in the screen shots of the "Hello World!" app in the first module of this course. There’s another theme installed by default in Ubuntu Touch, SuruDark. As its name says, this is a dark theme, with black backgrounds.

You can easily change the system theme on Ubuntu Touch with the app UT Tweak Tool. Go to System theme under the heading Experimental and choose SuruDark. You need to restart any open apps to use the selected theme.

So let’s remove all colors we’ve set in the shopping list app from the previous module:

  • Remove the backgroundColor property from the MainView.

  • Remove the StyleHints component from the PageHeader.

Now if you don’t explicitly set any colors in your app and you’ve activated SuruDark as your system theme on Ubuntu Touch, your app will automatically use the colors of this theme. You don’t need to add any code to your app for this.

If you want to test whether everything is clearly visible in your app with a dark theme, run Clickable as follows:

clickable desktop --dark-mode

The result looks like this for the shopping list app with explicit colors removed:

Your Clickable app shown in dark mode
Figure 1. Your Clickable app shown in dark mode

3. Exploring QML components

In the previous module of this course you learned how to use MainView, Page, PageHeader, Label, Button, TextField, ListModel, ListView, ListItem, and ListElement. Every time we introduced one of these components, we linked to its API documentation. I hope you already viewed these pages. If you’re going to develop your own apps, you’ll have to make yourself comfortable with the API documentation.

Let’s take a look at the Button component for an example. In our shopping list app from the previous module, we defined the Button like this:

Button {
	id: buttonAdd
	anchors {
		top: header.bottom
		right: parent.right
		topMargin: units.gu(2)
		rightMargin: units.gu(2)
	}
	text: i18n.tr('Add')
	onClicked: shoppinglistModel.append({"name": textFieldInput.text})
}

But how did we know what properties to add? For instance, how did we know that we had to assign the text to appear in the button to the property text?

If you look at the documentation page of Ubuntu.Components Button, you’ll see none of the properties we used, but it explains a couple of other properties, such as color, font, gradient, iconPosition, and strokeColor. Each of these is documented on that page.

For instance, the documentation of the strokeColor property says:

strokeColor : color

If set to a color, the button has a stroke border instead of a filled shape.

This QML property was introduced in Ubuntu.Components 1.1.

This tells you that you can set this property to an object of type color, such as UbuntuColors.orange or other properties of the Ubuntu.Components UbuntuColors component.

So now you know how you can adapt your buttons with these properties.

There’s also some example code showing the use of a button with various properties. Example code in QML documentation is a great way to see for yourself what these properties mean. Just copy/paste the example to your QML file, adapt where needed, and run clickable desktop to see the effects.

However, what with the properties we used but aren’t described on this page? Take a look at the top of the documentation page. There you have a table that says:

Inherits: AbstractButton

One component inherits from another component if it is a special type of this component. So this line says that the Button component is a special type of the AbstractButton component, which means that the former also has all the properties and signals of the latter.

Click on the AbstractButton link. This shows you the properties of the AbstractButton component, such as hovered, pressed and sensingMargins. It also shows some signals, like clicked and pressAndHold. We used the clicked signal in the following line of code:

onClicked: shoppinglistModel.append({"name": textFieldInput.text})

You see that the AbstractButton component also inherits from another component, ActionItem. Click on this ActionItem link. There you see some other properties, including the text property you used in your button.

anchors is a property of QtQuick Item, the base type that all visual items in Qt Quick inherit from.

So when you want to know what you can do with a QML component, first look up its documentation in the Ubuntu Touch API docs. Then check whether it inherits from another component and look at its documentation too, and repeat this process until you’re looking at a component that doesn’t inherit anymore from another one.

4. Positioning components in a row

In the previous module we positioned the TextField and Button components next to each other by anchoring them to the header and page, with the same top anchor.

However, there’s another way to position components next to each other: with a Row. For instance, this is how you position two buttons at the bottom of the page using a Row:

Row {
	spacing: units.gu(1)
	anchors {
		bottom: parent.bottom
		left: parent.left
		right: parent.right
		topMargin: units.gu(1)
		bottomMargin: units.gu(2)
		leftMargin: units.gu(2)
		rightMargin: units.gu(2)
	}
	Button {
		id: buttonRemoveAll
		text: i18n.tr("Remove all...")
		width: parent.width / 2 - units.gu(0.5)
	}
	Button {
		id: buttonRemoveSelected
		text: i18n.tr("Remove selected...")
		width: parent.width / 2 - units.gu(0.5)
	}
}

We use anchors to position the row. With the spacing property you specify the spacing between the elements in the row. Note that we don’t need any anchors in the Button components: we just specify their width, computed as half of the row’s width minus half of the spacing between the buttons.

The result looks like this:

Two buttons positioned in a row
Figure 2. Two buttons positioned in a row
There are other similar components to position components, such as Column to vertically position items, Grid to position items in a grid formation with rows and columns, and Flow that positions items side by side but wraps them to a next row if needed (like words on a page).

5. Adding popup dialogs

In the previous module we introduced the button to add the text in the text field to the list. This was done with just a simple handler for the onClicked signal of the Button component. However, sometimes you want to ask for a confirmation. That’s where a Dialog comes in: this forces the user to select a desired action, most of the times confirming or cancelling.

You can just specify the Dialog like we did until now in your Main.qml file. But because the dialogs for both buttons will be very similar, it’s better to define your own type of dialog in a separate QML file, and then instantiate two dialogs of your custom dialog type in your Main.qml file.

Create the file OKCancelDialog.qml in the qml directory, with the following contents:

import QtQuick 2.7
import Ubuntu.Components 1.3
import Ubuntu.Components.Popups 1.3

Dialog {
    id: dialog
    signal doAction()

    Button {
        text: i18n.tr("OK")
        color: theme.palette.normal.negative
        onClicked: {
            PopupUtils.close(dialog)
            doAction()
        }
    }

    Button {
        text: i18n.tr("Cancel")
        onClicked: PopupUtils.close(dialog)
    }
}
Dialog is a component of Ubuntu.Components.Popups, and that’s why you need to import this.

We give the Dialog an ID so we can refer to it later, and we also define a signal, doAction(). In previous QML code (and also later in this code) you used existing signals, such as clicked. However, you can also define your own signals, and that’s what we’re doing here.

Inside the Dialog component, we’re defining two buttons: one OK button and one button to cancel the dialog. Each of these buttons just has a text and a signal handler for the clicked signal. We also add a color for the OK button. This uses one of the theme’s colors. With theme.palette.normal we mean that this is the color for the button’s normal state, and negative is one of the possible colors in Ubuntu.Components.Themes PaletteValues.

According to the human interface guidelines for dialogs in Ubuntu Touch you need to highlight the main action using theme.palette.normal.negative if it is destructive, theme.palette.normal.positive if it is positive, and theme.palette.normal.focus if it is neutral.

The signal handler for the cancel button is easy:

onClicked: PopupUtils.close(dialog)

This means that we close the Dialog element with the id dialog if you click on the cancel button.

The signal handler for the OK button has to call two functions. That’s why you need the curly braces:

onClicked: {
	PopupUtils.close(dialog)
	doAction()
}

You can add as many statements as you want inside the curly braces. In this case we close the dialog and then call the handler for the doAction signal.

Now that you have defined your own custom QML type in OKCancelDialog.qml, you can use this in your Main.qml file. Add the following components inside your MainView:

Component {
	id: removeAllDialog

	OKCancelDialog {
		title: i18n.tr("Remove all items")
		text: i18n.tr("Are you sure?")
		onDoAction: console.log("Remove all items")
	}
}

Component {
	id: removeSelectedDialog

	OKCancelDialog {
		title: i18n.tr("Remove selected items")
		text: i18n.tr("Are you sure?")
		onDoAction: console.log("Remove selected items")
	}
}

As you see, you can refer to your custom QML type as OKCancelDialog, the name of the QML file in which it is defined. In both components you define the title and text, which are properties of the Dialog component.

With onDoAction: console.log("Remove all items"), you add a signal handler for the doAction signal you have defined in your custom type.

Now that you have defined these two dialogs, let’s see how you let them appear. In your button with id buttonRemoveAll, add the following signal handler:

onClicked: PopupUtils.open(removeAllDialog)

And in the button with id buttonRemoveSelected, add the following signal handler:

onClicked: PopupUtils.open(removeSelectedDialog)
Make sure to add import Ubuntu.Components.Popups 1.3 to the import statements in the beginning of your Main.qml file, otherwise you can’t use PopupUtils.

If you run this app now, you’ll see that clicking on the "Remove all…​" button opens a dialog asking you if you want to remove all items:

The dialog pops up to ask you to confirm or to cancel.
Figure 3. The dialog pops up to ask you to confirm or to cancel.

Clicking on the button next to it opens a dialog asking you if you want to remove the selected items. For now, clicking on OK in the dialog just logs a message to the console, and clicking on Cancel does nothing. In both cases, the dialog closes and you’re back at the main page of your app.

6. Adding leading actions to the list items

One of the characteristic user interface elements of Ubuntu Touch are the actions that appear when you swipe a list item left or right. These are ListItemActions. You can assign them to the properties leadingActions and trailingActions of the ListItem element.

Because of design constraints, you shouldn’t assign more than one leading action (to the left) or more than three trailing actions (to the right) to a list item.

As an example, let’s see how you create a leading action with a delete button to remove an item in the shopping list. Add the following code to the ListItem element:

leadingActions: ListItemActions {
	actions: [
		Action {
			iconName: "delete"
			onTriggered: shoppinglistModel.remove(index)
		}
	]
}

So you assign a ListItemActions element to the leadingActions property. This ListItemActions element has an actions property that contains a list of actions.

You construct a list with the brackets ([ and ]), and a comma between the elements in this list. In this case this list has just one element.

We assign an icon name to the action in this list. The value "delete" will show a trash icon. Then we set a signal handler for the triggered signal: in this case we call the remove method of our shopping list model (a ListModel element), with the index as the position of the current list item in the list.

If you run this app, add a few items to your shopping list. Then swipe one of the items to the right. A red trash icon appears to the left of the item:

You can delete an item in the shopping list with a leading action.
Figure 4. You can delete an item in the shopping list with a leading action.

If you click on it, it calls the function assigned to the triggered signal and deletes the item.

You can now do the same with trailing actions. In general leading actions are destructive, and trailing actions are informative. For instance, you could add an info icon to the trailing actions to show more information about an item.

7. Adding an action bar

A typical user interface element of Ubuntu Touch is an ActionBar. This shows a row of buttons that trigger actions, or an overflow button that triggers an overflow panel that shows the remaining actions.

An example will make this clear. Add the following ActionBar element to your PageHeader element, right below the subtitle property:

ActionBar {
	anchors {
		top: parent.top
		right: parent.right
		topMargin: units.gu(1)
		rightMargin: units.gu(1)
	}
	numberOfSlots: 1
	actions: [
		Action {
			iconName: "settings"
			text: i18n.tr("Settings")
		},
		Action {
			iconName: "info"
			text: i18n.tr("About")
		}
	]
}

This anchors the action bar to the top right of your page header.

Then we define the number of slots and a list of actions. The number of slots is the number of actions that are shown in the bar directly. If the actions don’t fit, an overflow button (using one slot) will be shown. If you click this button, this opens a panel with the remaining actions.

In this case we have two actions and one slot. So if you run this app, you’ll see the overflow button (with three horizontal stripes). If you click on it, you’ll see the two actions you’ve defined:

If you click on the action bar with one slot, both actions are shown in an overflow panel.
Figure 5. If you click on the action bar with one slot, both actions are shown in an overflow panel.

Now if you change numberOfSlots to 2 and run the app again, both actions are shown directly in the bar:

If the action bar has two slots, both actions are shown directly in the bar.
Figure 6. If the action bar has two slots, both actions are shown directly in the bar.
Setting numberOfSlots to 0 will always show the overflow button and no other action buttons. The Ambience theme sets the default value of numberOfSlots to 3.

Of course you want something to happen now when you click on one of the buttons in the action bar. Let’s create a simple about dialog that appears when you click on the About button.

Create a file AboutDialog.qml in your qml directory, with the following contents:

import QtQuick 2.7
import Ubuntu.Components 1.3
import Ubuntu.Components.Popups 1.3

Dialog {
    id: dialog
    title: i18n.tr("About")

    Label {
		width: parent.width
		wrapMode: Text.WordWrap
        text: i18n.tr("This is an example shopping list app designed to teach you Ubuntu Touch app development.")
    }

    Button {
        text: i18n.tr("Close")
        onClicked: PopupUtils.close(dialog)
    }
}

If you remember the OKCancelDialog from earlier in this module, this code should look familiar. However, instead of two buttons this dialog shows a label and a button.

Because the text in the label is too long to fit on one line, we need the text to wrap. This is done with the wrapMode property of the Text component (which the Label component inherits from). The value Text.WordWrap means that wrapping is done on word boundaries only.

Word wrapping only happens if you set an explicit width. That’s why the line width: parent.width is important here.

To use this dialog in your Main.qml file, define the following component in your MainView:

Component {
	id: aboutDialog
	AboutDialog {}
}

And then you can refer to this component by its ID aboutDialog. Do this by adding the following line to the about action:

onTriggered: PopupUtils.open(aboutDialog)

If you run this app, click on the action bar and then on the about icon, this shows the following dialog:

The about dialog appears with a click on the about icon in the action bar.
Figure 7. The about dialog appears with a click on the about icon in the action bar.

If you click on the Close button, the main page reappears.