Adding a dialog
Making our application useful
We have a window, cards and actions, but still no way to enter a name, description and date of our choice.
We could create a new page with the input fields, but a whole page just for a name, description and date seems excessive. Instead, we use a dialog.
Opening the dialog
pageStack.initialPage: Kirigami.ScrollablePage {
// ...
actions: [
Kirigami.Action {
id: addAction
icon.name: "list-add"
text: i18nc("@action:button", "Add kountdown")
onTriggered: addDialog.open()
}
]
}
First we change the action from the previous step: it is now just a Kirigami.Action that calls the dialog's open() function.
A dialog for adding countdowns
The new component is a Kirigami.Dialog. Dialogs appear in the middle of the window and are used for short tasks and extra information about the current content. They can't be moved, and they adapt their size to the window.
Kirigami.ApplicationWindow {
// ...
Kirigami.Dialog {
id: addDialog
title: i18nc("@title:window", "Add kountdown")
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
padding: Kirigami.Units.largeSpacing
preferredWidth: Kirigami.Units.gridUnit * 20
// Form layouts help align and structure a layout with several inputs
Kirigami.FormLayout {
// Textfields let you input text in a thin textbox
Controls.TextField {
id: nameField
// Provides a label attached to the textfield
Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
// What to do after input is accepted (i.e. pressed Enter)
// In this case, it moves the focus to the next field
onAccepted: descriptionField.forceActiveFocus()
}
Controls.TextField {
id: descriptionField
Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
placeholderText: i18n("Optional")
// Again, it moves the focus to the next field
onAccepted: dateField.forceActiveFocus()
}
Controls.TextField {
id: dateField
Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
// D means a required number between 1-9,
// 9 means a required number between 0-9
inputMask: "D999-99-99"
// Here we confirm the operation just like
// clicking the OK button
onAccepted: addDialog.accepted()
}
Controls.Label {
text: "* = required fields"
}
}
// The dialog logic goes here
}
// ...
}
A dialog has a header and a footer, inherited from Qt Quick Controls' Dialog.
By default the header shows the title and a close button, which you can hide with showCloseButton. The footer shows the buttons set with standardButtons (and customFooterActions); by default there are none.
We give our dialog an "OK" and a "Cancel" button, some padding, and a reasonable preferredWidth: the size the dialog expects to have, which can grow if needed. We use the Kirigami.Units we met before.
Then comes a Kirigami.FormLayout. Unlike a ColumnLayout, it arranges its children automatically and centered, with optional labels. As the name says, it is made for input forms.
Form layouts work with many kinds of input, but we stick to simple Controls.TextFields, plain text boxes.
The text fields are inputs for:
- the countdown's name;
- its description;
- the date we count down to, in the form
YYYY-MM-DD.
On each Controls.TextField we set Kirigami.FormData.label, the field's label. The form shows the labels to the left of the fields.
We also use onAccepted to call forceActiveFocus() on the next field, so pressing Enter moves to the next field, which makes the form easier to use.
The date field has an inputMask. D999-99-99 keeps users from entering anything that might break the application (such as text): only digits are allowed, which we can then try to turn into a date.
With the dialog's user interface done, we define its behavior. We need three things:
- enable the OK button only when the required fields are filled in;
- add the input to the model;
- clear the form.
Kirigami.Dialog {
// ...
// Once the Kirigami.Dialog is initialized,
// we want to create a custom binding to only
// make the Ok button visible if the required
// text fields are filled.
// For this we use Kirigami.Dialog.standardButton(button):
Component.onCompleted: {
const button = standardButton(Kirigami.Dialog.Ok);
// () => is a JavaScript arrow function
button.enabled = Qt.binding( () => requiredFieldsFilled() );
}
onAccepted: {
// The binding is created, but we still need to make it
// unclickable unless the fields are filled
if (!addDialog.requiredFieldsFilled()) return;
appendDataToModel();
clearFieldsAndClose();
}
}
First we bind the OK button's enabled property to a check of whether the fields are filled in. In JavaScript this needs Qt.binding(). In effect, the line
button.enabled = Qt.binding( () => requiredFieldsFilled() );
works like the QML bindings we have seen so far, as in this pseudo-code:
enabled: requiredFieldsFilled()
onAccepted handles the OK button. If the required fields aren't filled in, it returns without doing anything; otherwise it adds the input to the model and clears the dialog for the next time it is opened.
Kirigami.Dialog {
// ...
// We check that the nameField is not empty and that the
// dateField (which has an inputMask) is completely filled
function requiredFieldsFilled() {
return (nameField.text !== "" && dateField.acceptableInput);
}
function appendDataToModel() {
kountdownModel.append({
name: nameField.text,
description: descriptionField.text,
date: new Date(dateField.text)
});
}
function clearFieldsAndClose() {
nameField.text = ""
descriptionField.text = ""
dateField.text = ""
addDialog.close();
}
}
For the required name field, we just check that its text isn't empty. The date field has an input mask, so we check acceptableInput instead, which is only true once the whole field is filled with acceptable characters.
Then the append() method of kountdownModel adds a JavaScript object with these properties; the date is stored as a JavaScript Date.
Finally we clear the text fields by setting their text to an empty string, and close() the dialog.
Save the files and build the program, and you can add your own countdowns! A few finishing touches improve the interface. First, remove the placeholder countdown from the previous steps; the model now starts empty:
ListModel {
id: kountdownModel
}
Second, now that we have a real date, show how many days are left until it:
Kirigami.Heading {
level: 1
text: i18n("%1 days", Math.round((date-Date.now())/86400000))
}
Third, make the window bigger, so there is more room for the new cards:
width: 600
height: 400
Much nicer.
Our application so far
Main.qml:
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
Kirigami.ApplicationWindow {
id: root
width: 600
height: 400
title: i18nc("@title:window", "Day Kountdown")
globalDrawer: Kirigami.GlobalDrawer {
isMenu: true
actions: [
Kirigami.Action {
text: i18n("Quit")
icon.name: "application-exit-symbolic"
shortcut: StandardKey.Quit
onTriggered: Qt.quit()
}
]
}
ListModel {
id: kountdownModel
}
Component {
id: kountdownDelegate
Kirigami.AbstractCard {
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
GridLayout {
id: delegateLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
}
rowSpacing: Kirigami.Units.largeSpacing
columnSpacing: Kirigami.Units.largeSpacing
columns: root.wideScreen ? 4 : 2
Kirigami.Heading {
level: 1
text: i18n("%1 days", Math.round((date-Date.now())/86400000))
}
ColumnLayout {
Kirigami.Heading {
Layout.fillWidth: true
level: 2
text: name
}
Kirigami.Separator {
Layout.fillWidth: true
visible: description.length > 0
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: description
visible: description.length > 0
}
}
Controls.Button {
Layout.alignment: Qt.AlignRight
Layout.columnSpan: 2
text: i18n("Edit")
}
}
}
}
}
Kirigami.Dialog {
id: addDialog
title: i18nc("@title:window", "Add kountdown")
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
padding: Kirigami.Units.largeSpacing
preferredWidth: Kirigami.Units.gridUnit * 20
// Form layouts help align and structure a layout with several inputs
Kirigami.FormLayout {
// Textfields let you input text in a thin textbox
Controls.TextField {
id: nameField
// Provides a label attached to the textfield
Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
// What to do after input is accepted (i.e. pressed Enter)
// In this case, it moves the focus to the next field
onAccepted: descriptionField.forceActiveFocus()
}
Controls.TextField {
id: descriptionField
Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
placeholderText: i18n("Optional")
// Again, it moves the focus to the next field
onAccepted: dateField.forceActiveFocus()
}
Controls.TextField {
id: dateField
Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
// D means a required number between 1-9,
// 9 means a required number between 0-9
inputMask: "D999-99-99"
// Here we confirm the operation just like
// clicking the OK button
onAccepted: addDialog.accepted()
}
Controls.Label {
text: "* = required fields"
}
}
// Once the Kirigami.Dialog is initialized,
// we want to create a custom binding to only
// make the Ok button visible if the required
// text fields are filled.
// For this we use Kirigami.Dialog.standardButton(button):
Component.onCompleted: {
const button = standardButton(Kirigami.Dialog.Ok);
// () => is a JavaScript arrow function
button.enabled = Qt.binding( () => requiredFieldsFilled() );
}
onAccepted: {
// The binding is created, but we still need to make it
// unclickable unless the fields are filled
if (!addDialog.requiredFieldsFilled()) return;
appendDataToModel();
clearFieldsAndClose();
}
// We check that the nameField is not empty and that the
// dateField (which has an inputMask) is completely filled
function requiredFieldsFilled() {
return (nameField.text !== "" && dateField.acceptableInput);
}
function appendDataToModel() {
kountdownModel.append({
name: nameField.text,
description: descriptionField.text,
date: new Date(dateField.text)
});
}
function clearFieldsAndClose() {
nameField.text = ""
descriptionField.text = ""
dateField.text = ""
addDialog.close();
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
title: i18nc("@title", "Kountdown")
// Kirigami.Action encapsulates a UI action. Inherits from Controls.Action
actions: [
Kirigami.Action {
id: addAction
// Name of icon associated with the action
icon.name: "list-add-symbolic"
// Action text, i18n function returns translated string
text: i18nc("@action:button", "Add kountdown")
// What to do when triggering the action
onTriggered: addDialog.open()
}
]
Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: kountdownDelegate
}
}
}