Uso de archivos y señales separados

Separar código difícil de manejar en diferentes archivos y adjuntar señales a los componentes.

Pero ¿por qué?

Por primera vez, separaremos algunos de nuestros componentes en sus propios archivos QML. Si seguimos añadiendo cosas a main.qml, rápidamente se volverá difícil saber qué hace cada cosa, y corremos el riesgo de enturbiar nuestro código.

Primero necesitamos añadir los nuevos archivos a nuestro resources.qrc que creamos en la primera parte de este tutorial.

<RCC>
    <qresource prefix="/">
        <file alias="main.qml">contents/ui/main.qml</file>
        <file alias="AddEditSheet.qml">contents/ui/AddEditSheet.qml</file>
        <file alias="KountdownDelegate.qml">contents/ui/KountdownDelegate.qml</file>
    </qresource>
</RCC>

Uso de nuestros nuevos archivos

Necesitaremos encontrar algún modo de usar los nuevos archivos en main.qml. Afortunadamente, todo lo que tenemos que hacer es incluir una declaración de dichos componentes en nuestro main.qml del siguiente modo:

AddEditSheet {
    id: addEditSheet
}

Extendiendo nuestra hoja de añadir a una hoja de añadir/editar

Mientras que en el último tutorial hicimos que nuestro botón para añadir una cuenta atrás hiciera algo, el botón de edición de nuestras tarjetas de cuenta atrás sigue inactivo. También creamos una hoja para añadir que ahora podríamos reutilizar para que también sirva como una hoja de edición... Pero antes de esto necesitamos añadir un par de cosas adicionales a nuestro main.qml.

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
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

Kirigami.ApplicationWindow {
    id: root

    title: i18nc("@title:window", "Day Kountdown")

    globalDrawer: Kirigami.GlobalDrawer {
        isMenu: true
        actions: [
            Kirigami.Action {
                text: i18n("Quit")
                icon.name: "gtk-quit"
                shortcut: StandardKey.Quit
                onTriggered: Qt.quit()
            }
        ]
    }

    ListModel {
        id: kountdownModel
    }

    // Fetches item from addEditSheet.qml and does action on signal
    AddEditSheet {
        id: addEditSheet
        onAdded: kountdownModel.append({
            "name": name,
            "description": description,
            "date": Date.parse(kdate)
        });
        onEdited: kountdownModel.set(index, {
            "name": name,
            "description": description,
            "date": Date.parse(kdate)
        });
        onRemoved: kountdownModel.remove(index, 1)
    }

    // Function called by 'edit' button on card and by 'add'-Action
    function openPopulatedSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
        addEditSheet.mode = mode
        addEditSheet.index = index;
        addEditSheet.name = listName
        addEditSheet.description = listDesc
        addEditSheet.kdate = listDate

        addEditSheet.open()
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        title: i18nc("@title", "Kountdown")

        // Kirigami.Action encapsulates a UI action. Inherits from Controls.Action
        actions.main: Kirigami.Action {
            id: addAction
            // Name of icon associated with the action
            icon.name: "list-add"
            // Action text, i18n function returns translated string
            text: i18nc("@action:button", "Add kountdown")
            // What to do when triggering the action
            onTriggered: openPopulatedSheet("add")
        }

        Kirigami.CardsListView {
            id: layout
            model: kountdownModel
            delegate: KountdownDelegate {}
        }
    }
}

Los cambios claves que hemos realizado implican la adición de nuestra definición de componente AddEditSheet (y KountdownDelegate más abajo) y una nueva función llamada openPopulatedSheet().

Veamos nuestra definición de AddEditSheet:

AddEditSheet { 
    id: addEditSheet
    onEdited: kountdownModel.set(index, {
        name,
        description,
        date,
    });
    onAdded: kountdownModel.append({
        name,
        description,
        date,
    });
}

onAdded and onEdited are signal handlers. Just like onTriggered is called when we click an action, we can use handlers that respond to our custom signals.

AddEditSheet.qml

Looking at our new AddEditSheet.qml—our repurposed adding sheet—we can see how these signals work:

 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

// Overlay sheets appear over a part of the window
Kirigami.OverlaySheet {
    id: addEditSheet

    // Sheet mode
    property string mode: "add"

    property int index: -1
    property alias name: nameField.text
    property alias description: descriptionField.text
    property alias kdate: dateField.text

    // Signals can be read and certain actions performed when these happen
    signal added (string name, string description, var kdate)
    signal edited(int index, string name, string description, var kdate)
    signal removed(int index)

    header: Kirigami.Heading {
        // i18nc is useful for adding context for translators
        text: mode === "add" ? i18nc("@title:window", "Add kountdown") :
            i18nc("@title:window", "Edit kountdown")
    }
    // 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 label attached to the textfield
            Kirigami.FormData.label: i18nc("@label:textbox", "Name:")
            // Placeholder text is visible before you enter anything
            placeholderText: i18n("Event name (required)")
            // 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")
            onAccepted: dateField.forceActiveFocus()
        }
        Controls.TextField {
            id: dateField
            Kirigami.FormData.label: i18nc("@label:textbox", "Date:")
            inputMask: "0000-00-00"
            placeholderText: i18n("YYYY-MM-DD")
        }
        // This is a button.
        Controls.Button {
            id: deleteButton
            Layout.fillWidth: true
            text: i18nc("@action:button", "Delete")
            visible: mode === "edit"
            onClicked: {
                addEditSheet.removed(addEditSheet.index)
                close();
            }
        }
        Controls.Button {
            id: doneButton
            Layout.fillWidth: true
            text: i18nc("@action:button", "Done")
            // Button is only enabled if the user has entered something into the nameField
            enabled: nameField.text.length > 0
            onClicked: {
                // Add a listelement to the kountdownModel ListModel
                if(mode === "add") {
                    addEditSheet.added(
                        nameField.text,
                        descriptionField.text,
                        dateField.text
                    );
                }
                else {
                    addEditSheet.edited(
                        index,
                        nameField.text,
                        descriptionField.text,
                        dateField.text
                    );
                }
                close();
            }
        }
    }
}

Signals invoke their handlers when they are called. In this case, we have created two signals, added and edited, that we can invoke with different outcomes, and to which we can attach information about the countdown we are adding or creating. A neat thing about signals is that they expose the variables defined in them to the functions that are listening to them, which is why we can just call those variable names in our onEdited and onAdded handlers in main.qml. Our signals are invoked by the "Done" button depending on what the mode property, defined at the top of our AddEditSheet, is set to.

La propiedad mode también controla varias cosas más: principalmente a qué está asignado el título de nuestra hoja y qué texto se incluirá en nuestros campos de texto. Sin embargo, de forma predeterminada, nuestra propiedad mode está asignada para añadir...

Which brings us back to main.qml and our new openPopulatedSheet() function. You might have noticed that this is what it is called now when the countdown-adding action is triggered. This function takes in several arguments which have been provided with defaults. This is helpful when we simply want to add a new countdown, because we can have the concise function call openPopulatedSheet("add"). More importantly, this function sets all the relevant properties in AddEditSheet.

function openPopulatedSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
    addEditSheet.mode = mode
    addEditSheet.index = index;
    addEditSheet.name = listName
    addEditSheet.description = listDesc
    addEditSheet.kdate = listDate

    addEditSheet.open()
}
  • mode cambia la hoja de añadir/editar dependiendo de si este argumento está definido como "añadir" o como "editar"
  • index es necesario para que cuando guardemos nuestra cuenta atrás editada se modifique la correcta.
  • listName, listDesc y listDate son los detalles relevantes de la cuenta atrás que necesita poner en los campos de la hoja

Of course, to actually use our sheet for anything besides adding countdowns first we need to make the edit button on our cards work. But if you look at our Kirigami.CardsListView in main.qml...

Kirigami.CardsListView {
    id: layout
    model: kountdownModel
    delegate: KountdownDelegate {}
}

KountdownDelegate.qml

We've replaced our Kirigami.AbstractCard with a delegate component definition from KountdownDelegate.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
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

Kirigami.AbstractCard {
    id: kountdownDelegate
    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 {
                Layout.fillHeight: true
                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
                // Column spanning within grid layout (vertically in this case)
                Layout.columnSpan: 2
                text: i18n("Edit")
                onClicked: openPopulatedSheet("edit", index, name, description, new Date(date).toISOString().slice(0,10))
            }
        }
    }
}

The onClicked property of the "Edit" button on our cards now calls the openPopulatedSheet() function, with the card's fetched list element properties set as the arguments for this function. With these, the sheet can be populated with the correct text.

Con eso, ¡tenemos una hoja completamente funcional con la que podemos añadir y editar nuestras cuentas atrás!