Додавання діалогового вікна

Знайомимося із діалоговими вікнами Kirigami.

Робимо нашу програму корисною

У нас є вікно, у нас є картки і у нас є дії. Але нам усе ще потрібне спосіб введення назви, опису та вибраної дати.

Одним зі способів досягти потрібного результату є створення сторінки, на якій ми розташуємо потрібні нам елементи для введення даних. Втім, надавати цілу сторінку для введення назви, опис та дати забагато.

Замість цього ми скористаємося діалоговим вікном.

Діалогове вікно, що з'являється посередині вікна програми

Відкриття діалогового вікна

pageStack.initialPage: Kirigami.ScrollablePage {
    // ...
    actions: [
        Kirigami.Action {
            id: addAction
            icon.name: "list-add"
            text: i18nc("@action:button", "Add kountdown")
            onTriggered: addDialog.open()
        }
    ]
}

Спочатку внесемо зміни до дії з попереднього розділу підручника — просто додамо Kirigami.Action, яка вмикає функцію open() діалогового вікна.

Зворотний відлік — додавання діалогових вікон

Новим компонентом, який ми додаємо є Kirigami.Dialog. Діалогові вікна з'являються у центрі вікна. Ними можна скористатися для надання додаткових відомостей, які пов'язано із поточним вмістом вікна. Їх не можна пересувати, але їхній розмір адаптується до розміру головного вікна.

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

        // Компонування форми допомагають вирівняти і структурувати компонування із декількома
        // елементами введення
        Kirigami.FormLayout {
            // Об'єкти Textfield надають вам змогу вводити текст у вузькому полі для введення тексту
            Controls.TextField {
                id: nameField
                // Надає доступ до мітки, яку пов'язано із текстовим полем
                Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
                // Що робити після прийняття вхідних даних (тобто натискання клавіші Enter)? У цьому
                // випадку фокусування буде пересунуто на наступне поле
                onAccepted: descriptionField.forceActiveFocus()
            }
            Controls.TextField {
                id: descriptionField
                Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
                placeholderText: i18n("Optional")
                // Знову ж таки, пересуває фокусування до наступного поля
                onAccepted: dateField.forceActiveFocus()
            }
            Controls.TextField {
                id: dateField
                Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
                // D означає обов'язкове число у діапазоні 1-9, 9 — обов'язкове число у діапазоні
                // 0-9
                inputMask: "D999-99-99"
                // Тут ми підтверджуємо дію, подібно до натискання кнопки «Гаразд»
                onAccepted: addDialog.onAccepted()
            }
            Controls.Label {
                text: "* = required fields"
            }
        }
        // Тут маємо логіку діалогового вікна
    }
    // ...
}

У діалогових вікон, типово, є заголовок і підвал, обидва успадковані від Controls.Dialog.

До заголовка, типово, включають назву і кнопку закриття, яку можна вимкнути за допомогою showCloseButton. До підвалу, типово, включено кнопку закриття, і цю поведінку можна перевизначити за допомогою standardButtons.

Спочатку, ми налаштуємо його на показ кнопки «Гаразд» і «Скасувати», додамо фаски, та додамо прийнятну preferredWidth. Пріоритетною шириною є типовий очікуваний розмір діалогового вікна. Її може бути збільшено, якщо це потрібно. Ми можемо скористатися стандартним Kirigami.Units, до якого ми ще звернемося пізніше.

Далі, ми переходимо до Kirigami.FormLayout. На відміну від ColumnLayout, компонування його дочірніх компонентів є автоматичним і центрованим з необов'язковими мітками. Як можна зрозуміти з назви, його використовують для створення форм введення даних.

Ці компонування форм розроблено для роботи із різними типами полів введення, хоча тут ми використовуємо прості поля Controls.Textfield, якщо відповідають простим текстовим блоками для вписування простих текстових фрагментів.

Нами створено елементи Textfield, які працюють як:

  1. Поле для введення назви нашого відліку
  2. Поле для введення опису нашого відліку
  3. Поле для введення дати, до якої ми ведемо відлік і яка надається у форматі YYYY-MM-DD

У кожному з цих елементів Controls.Textfield ми встановлюємо властивість Kirigami.FormData.label, що надає нам змогу визначити для них мітки. Форма покаже належні мітки ліворуч від кожного з цих полів для введення тексту.

Нарешті, ми також встановлюємо властивість onAccepted для вмикання методу forceActiveFocus() наступного поля; це призведе до перемикання активного поля, щойно користувач натисне клавішу ENTER, удосконалюючи зручність форми.

Ми також встановлюємо для текстового поля для нашої дати властивість із назвою inputMask. Встановлення значення D999-99-99 забороняє користувачеві вводити будь-що, що може зашкодити нормальній роботі програми (наприклад, звичайний текст). Користувач зможе вводити лише цифри, які згодом може бути перетворено на об'єкт дати.

Щойно роботу над інтерфейсом користувача діалогового вікна завершено, нам потрібно змінити поведінку вікна. Для цього нам потрібно три речі:

  1. Показ кнопки «Гаразд», лише якщо заповнено обов'язкові поля
  2. Додавання вхідних даних до моделі
  3. Очищення форми введення даних
Kirigami.Dialog {
    // …Щойно Kirigami.Dialog буде ініціалізовано, нам потрібно буде створити нетипову прив'язку
    // такого різновиду, що кнопка «Гаразд» стала видимою, лише якщо буде заповнено обов'язкові
    // текстові поля. Для цього ми скористаємося Kirigami.Dialog.standardButton(button):
    Component.onCompleted: {
        const button = standardButton(Kirigami.Dialog.Ok);
        // () => є функцією стрілочки JavaScript
        button.enabled = Qt.binding( () => requiredFieldsFilled() );
    }
    onAccepted: {
        // Прив'язку створено, але нам усе ще потрібно зробити її непридатною до клацання, якщо не
        // заповнено поля
        if (!addDialog.requiredFieldsFilled()) return;
        appendDataToModel();
        clearFieldsAndClose();
    }
}

Першим, що слід зробити, є створення зв'язку між властивістю enabled кнопки «Гаразд» і перевіркою того, чи заповнено поля, що у цьому випадку слід зробити за допомогою Qt.binding() у JavaScript. У результаті рядок:

button.enabled = Qt.binding( () => requiredFieldsFilled() );

є подібним до прив'язок QML, які ми досі мали, як у такому псевдокоді:

enabled: requiredFieldsFilled()

Обробником сигналів, який вмикатиме кнопку «Гаразд», є onAccepted. Він лишається порожнім і таким, що не виконуватиме ніяких дій, якщо обов'язкові поля заповнено; у інших випадках він додаватиме поле введення до моделі і спорожнятиме діалогове вікно для наступного моменту, коли його буде відкрито.

Kirigami.Dialog {
    // …Ми перевіряємо, чи є непорожнім nameField, і що dateField (для якого визначено inputMask) є
    // повністю заповненим
    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();
    }
}

Для нашого обов'язкового поля name нам достатньо перевірити, чи є текстом поля порожній рядок. Для поля date, оскільки для нього визначено маску введення, замість цього, нам слід скористатися acceptableInput, значенням якого стане true, щойно усе поле буде заповнено, і у ньому міститимуться лише прийнятні символи.

Далі, метод append() нашої моделі списку kountdownModel додає об'єкт JavaScript, включно із наданими нами властивостями.

Нарешті, ми спорожняємо текстові поля встановленням для їхніх властивостей text значення порожнього рядка, а потім виконуємо close().

Після зберігання наших файлів і збирання нашої програми ми зможемо додати наші власні зворотні відліки! Ми можемо додати останній штрих для удосконалення інтерфейсу, а саме вилучити фіктивний зворотний відлік, який ми мали у попередніх уроках:

26
27
28
    ListModel {
        id: kountdownModel
    }

По-друге, тепер, коли у нас є справжня дата, ми можемо обчислити час до цієї дати:

47
48
49
50
                    Kirigami.Heading {
                        level: 1
                        text: i18n("%1 days", Math.round((date-Date.now())/86400000))
                    }

І по-третє, збільшуємо розмір вікна, щоб у нас було місце для наших нових карток:

 9
10
    width: 600
    height: 400

Набагато краще.

Наша програма на поточний момент

Main.qml:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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.onAccepted()
            }
            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
        }
    }
}

Знімок вікна програми з чотирма картками-прикладами