diff --git a/examples/todo/CMakeLists.txt b/examples/todo/CMakeLists.txt index 5cda897..de73e3c 100644 --- a/examples/todo/CMakeLists.txt +++ b/examples/todo/CMakeLists.txt @@ -2,6 +2,13 @@ cmake_minimum_required(VERSION 3.1.0) project(todo) +################# set KDE specific information ################# +find_package(ECM REQUIRED NO_MODULE) + +# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) + + set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -12,10 +19,8 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5Qml CONFIG REQUIRED) -find_package(Qt5Gui CONFIG REQUIRED) -find_package(Qt5Core CONFIG REQUIRED) -find_package(Qt5Quick CONFIG REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml QuickControls2) +find_package(KF5 REQUIRED COMPONENTS Kirigami2 I18n) set(todo_SRCS main.cpp @@ -40,4 +45,4 @@ add_executable(todo WIN32 ${todo_SRCS}) add_dependencies(todo QuickFlux) target_link_libraries(todo debug quickfluxd optimized quickflux) -target_link_libraries(todo Qt5::Qml Qt5::Gui Qt5::Core Qt5::Quick) +target_link_libraries(todo Qt5::Qml Qt5::Gui Qt5::Core Qt5::Quick KF5::I18n) diff --git a/examples/todo/MainPage.qml b/examples/todo/MainPage.qml new file mode 100644 index 0000000..e91d358 --- /dev/null +++ b/examples/todo/MainPage.qml @@ -0,0 +1,20 @@ +import org.kde.kirigami 2.2 as Kirigami +import "./views" + +Kirigami.ScrollablePage { + id: page + + // space for title will be reserved anyway, + // so why not just fill it with something meaningful + title: "Todo list" + + header: Header {} + footer: Footer {} + + // page content item goes inline + TodoList {} + + // Kirigami.ScrollablePage has undocumented feature: + // it looks differently when the child item is ListModel + Kirigami.Theme.colorSet: Kirigami.Theme.Window +} diff --git a/examples/todo/MainWindow.qml b/examples/todo/MainWindow.qml index e6ff1d0..798ff4f 100644 --- a/examples/todo/MainWindow.qml +++ b/examples/todo/MainWindow.qml @@ -1,35 +1,9 @@ -import QtQuick 2.3 -import QtQuick.Window 2.2 -import QtQuick.Layouts 1.0 -import QuickFlux 1.1 -import "./views" -import "./middlewares" -import "./actions" +import org.kde.kirigami 2.0 as Kirigami -Window { - width: 480 - height: 640 - visible: true +// Wraps single-page application in a platform window +Kirigami.ApplicationWindow { + width: 18 * Kirigami.Units.gridUnit + height: 20 * Kirigami.Units.gridUnit - ColumnLayout { - anchors.fill: parent - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Header { - Layout.fillWidth: true - Layout.fillHeight: false - } - - TodoList { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Footer { - Layout.fillWidth: true - Layout.fillHeight: false - } - } + pageStack.initialPage: MainPage {} } - diff --git a/examples/todo/actions/ActionTypes.qml b/examples/todo/actions/ActionTypes.qml index 82bbd2a..abd9cdb 100644 --- a/examples/todo/actions/ActionTypes.qml +++ b/examples/todo/actions/ActionTypes.qml @@ -1,6 +1,6 @@ pragma Singleton import QtQuick 2.0 -import QuickFlux 1.0 +import QuickFlux 1.1 KeyTable { // KeyTable is an object with properties equal to its key name @@ -13,4 +13,3 @@ KeyTable { property string startApp } - diff --git a/examples/todo/actions/AppActions.qml b/examples/todo/actions/AppActions.qml index 3a99d45..e863c15 100644 --- a/examples/todo/actions/AppActions.qml +++ b/examples/todo/actions/AppActions.qml @@ -1,7 +1,6 @@ pragma Singleton import QtQuick 2.0 import QuickFlux 1.1 -import "./" ActionCreator { @@ -9,10 +8,9 @@ ActionCreator { signal addTask(string title); // Set/unset done on a task - signal setTaskDone(var uid, bool done) + signal setTaskDone(int uid, bool done) // Show/hide completed task - signal setShowCompletedTasks(bool value) + signal setShowCompletedTasks(bool value, Item contextItem) } - diff --git a/examples/todo/main.cpp b/examples/todo/main.cpp index 30f14a9..c347aa3 100644 --- a/examples/todo/main.cpp +++ b/examples/todo/main.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -12,6 +14,8 @@ int main(int argc, char *argv[]) registerQuickFluxQmlTypes(); // It is not necessary to call this function if the QuickFlux library is installed via qpm QQmlApplicationEngine engine; + + engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); QFAppDispatcher* dispatcher = QFAppDispatcher::instance(&engine); @@ -19,4 +23,3 @@ int main(int argc, char *argv[]) return app.exec(); } - diff --git a/examples/todo/main.qml b/examples/todo/main.qml index a122e62..5256bde 100644 --- a/examples/todo/main.qml +++ b/examples/todo/main.qml @@ -1,11 +1,9 @@ import QtQuick 2.3 -import QtQuick.Window 2.2 -import QtQuick.Layouts 1.0 import QuickFlux 1.1 -import "./views" import "./middlewares" import "./actions" +// Non-visual container for application-wide components Item { MiddlewareList { @@ -23,4 +21,3 @@ Item { id: mainWindow } } - diff --git a/examples/todo/middlewares/DialogMiddleware.qml b/examples/todo/middlewares/DialogMiddleware.qml index 722895b..b817c05 100644 --- a/examples/todo/middlewares/DialogMiddleware.qml +++ b/examples/todo/middlewares/DialogMiddleware.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 +import QtQuick.Controls 2.1 import QuickFlux 1.1 -import QtQuick.Dialogs 1.2 import "../actions" import "../stores" @@ -8,20 +8,48 @@ Middleware { property RootStore store: MainStore - MessageDialog { + Dialog { id: dialog - title: "Confirmation" - text: "Are you sure want to show completed tasks?" - standardButtons: StandardButton.Ok | StandardButton.Cancel + modal: true + anchors.centerIn: parent + + title: i18n("Confirmation") + Label { + text: i18n("Are you sure want to show completed tasks?") + } + standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: { + _cleanup(); next(ActionTypes.setShowCompletedTasks, {value: true}); } onRejected: { + _cleanup(); /// Trigger the changed signal even it is unchanged. It forces the checkbox to be turned off. store.userPrefs.showCompletedTasksChanged(); } + + function reparent(item) { + // attach dialog to the root item, so that it stays centerred + parent = _findRoot(item) + } + + // Drills up to the top of component hierarchy. + function _findRoot(/* Item */ item) { + if (item === null) { + return null; + } + while (item.parent !== null) { + item = item.parent; + } + return item; + } + + function _cleanup() { + // free the parent + reparent(null); + } } function dispatch(type, message) { @@ -29,6 +57,7 @@ Middleware { if (type === ActionTypes.setShowCompletedTasks && message.value === true) { // If user want to show completed tasks, drop the action and show a dialog + dialog.reparent(message.contextItem); dialog.open(); return; } @@ -36,5 +65,4 @@ Middleware { /// Pass the action to next middleware / store next(type, message); } - } diff --git a/examples/todo/middlewares/SystemMiddleware.qml b/examples/todo/middlewares/SystemMiddleware.qml index 192de63..56084aa 100644 --- a/examples/todo/middlewares/SystemMiddleware.qml +++ b/examples/todo/middlewares/SystemMiddleware.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 +import QtQuick.Window 2.0 import QuickFlux 1.1 -import QtQuick.Dialogs 1.2 import "../actions" import "../stores" @@ -8,7 +8,7 @@ Middleware { property RootStore store: MainStore - property var mainWindow: null + property Window mainWindow: null function dispatch(type, message) { if (type === ActionTypes.startApp) { @@ -25,5 +25,4 @@ Middleware { console.log("closing"); } } - } diff --git a/examples/todo/qml.qrc b/examples/todo/qml.qrc index 571e503..4e773cc 100644 --- a/examples/todo/qml.qrc +++ b/examples/todo/qml.qrc @@ -17,5 +17,6 @@ middlewares/DialogMiddleware.qml MainWindow.qml middlewares/SystemMiddleware.qml + MainPage.qml diff --git a/examples/todo/stores/MainStore.qml b/examples/todo/stores/MainStore.qml index ef9e309..e4dedb2 100644 --- a/examples/todo/stores/MainStore.qml +++ b/examples/todo/stores/MainStore.qml @@ -1,5 +1,4 @@ pragma Singleton -import QtQuick 2.0 import QuickFlux 1.1 RootStore { diff --git a/examples/todo/stores/RootStore.qml b/examples/todo/stores/RootStore.qml index 1c1c429..310b413 100644 --- a/examples/todo/stores/RootStore.qml +++ b/examples/todo/stores/RootStore.qml @@ -1,4 +1,3 @@ -import QtQuick 2.0 import QuickFlux 1.1 Store { diff --git a/examples/todo/stores/TodoStore.qml b/examples/todo/stores/TodoStore.qml index 73f77a0..6720dc5 100644 --- a/examples/todo/stores/TodoStore.qml +++ b/examples/todo/stores/TodoStore.qml @@ -51,29 +51,26 @@ Store { type: ActionTypes.addTask onDispatched: { - var item = { + const item = { uid: nextUid++, title: message.title, done: false } - model.append(item); + model.insert(0, item); } } Filter { type: ActionTypes.setTaskDone onDispatched: { - for (var i = 0 ; i < model.count ; i++) { - var item = model.get(i); + // quick and dirty + for (let i = 0; i < model.count; i++) { + const item = model.get(i); if (item.uid === message.uid) { - model.setProperty(i,"done",message.done); + model.setProperty(i, "done", message.done); break; } } } } - } - - - diff --git a/examples/todo/stores/UserPrefsStore.qml b/examples/todo/stores/UserPrefsStore.qml index b9b755b..e4d421a 100644 --- a/examples/todo/stores/UserPrefsStore.qml +++ b/examples/todo/stores/UserPrefsStore.qml @@ -1,4 +1,3 @@ -import QtQuick 2.0 import QuickFlux 1.1 import "../actions" diff --git a/examples/todo/views/Footer.qml b/examples/todo/views/Footer.qml index 80e505d..fb11b96 100644 --- a/examples/todo/views/Footer.qml +++ b/examples/todo/views/Footer.qml @@ -1,32 +1,44 @@ -import QtQuick 2.0 -import QtQuick.Controls 1.0 +import QtQuick 2.1 +import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 +import org.kde.kirigami 2.7 as Kirigami import "../actions" -Item { - height: 56 +RowLayout { + id: row - function add() { - AppActions.addTask(textField.text); - textField.text = ""; + width: parent.width + implicitHeight: row.implicitHeight + + spacing: Kirigami.Units.smallSpacing + + Label { + text: i18n("To do:") + + leftPadding: Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.smallSpacing } - RowLayout { - anchors.fill: parent + Kirigami.ActionTextField { + id: textField + Layout.fillWidth: true - TextField { - id: textField - Layout.fillWidth: true - focus: true - onAccepted: add(); - } + placeholderText: i18n("New task...") - Button { - text: "ADD" - onClicked: { - add(); + focus: true + onAccepted: add(); + rightActions: Kirigami.Action { + text: i18n("Add") + // icon from Breeze (breeze-icons) package + iconName: "list-add" + visible: textField.text !== "" + onTriggered: { + textField.add(); } } + function add() { + AppActions.addTask(textField.text); + textField.text = ""; + } } } - diff --git a/examples/todo/views/Header.qml b/examples/todo/views/Header.qml index e0404dd..6888189 100644 --- a/examples/todo/views/Header.qml +++ b/examples/todo/views/Header.qml @@ -1,28 +1,28 @@ import QtQuick 2.0 -import QtQuick.Controls 1.0 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.0 +import org.kde.kirigami 2.0 as Kirigami import "../actions" import "../stores" -Item { - height: 48 +// It fills width of parent, and uses as much height as needed. +RowLayout { + width: parent.width + + Label { + text: i18n("Items:") + leftPadding: Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.smallSpacing + } CheckBox { id: checkBox - checked: false - text: "Show Completed"; - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + text: i18n("Show Completed"); + checked: MainStore.userPrefs.showCompletedTasks onCheckedChanged: { - AppActions.setShowCompletedTasks(checked); - } - - Binding { - target: checkBox - property: "checked" - value: MainStore.userPrefs.showCompletedTasks + AppActions.setShowCompletedTasks(checked, this); } } - } - diff --git a/examples/todo/views/TodoItem.qml b/examples/todo/views/TodoItem.qml index b20d45c..983d6bc 100644 --- a/examples/todo/views/TodoItem.qml +++ b/examples/todo/views/TodoItem.qml @@ -1,33 +1,74 @@ import QtQuick 2.0 -import QtQuick.Controls 1.0 -import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.0 +import org.kde.kirigami 2.2 as Kirigami import "../actions" -Rectangle { +Item { id: item - color: "white" - height: 48 + + width: parent.width + implicitHeight: 48 property int uid; - property string title + property alias title: checkBox.text property alias checked: checkBox.checked - RowLayout { + CheckBox { + id: checkBox anchors.fill: parent + } + + onCheckedChanged: { + AppActions.setTaskDone(uid, checked); + } + + // QFlux is compatible with default QML states and transitions. + // However, use them with care. In particular, avoid any complex + // logic in PropertyChanges updates. + Item { + // this is just a layout container for the animated component + id: container + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Kirigami.Units.smallSpacing + anchors.verticalCenter: parent.verticalCenter + height: 2 - CheckBox { - id: checkBox - anchors.verticalCenter: parent.verticalCenter + // animated component + Rectangle { + id: strikethrough + width: 0 + height: parent.height + + color: Kirigami.Theme.disabledTextColor + z: 1 + } + + // animation states & transitions + states: State { + name: "checked" + when: item.checked + PropertyChanges { + target: strikethrough + width: container.width + } } - Text { - text: title - Layout.fillWidth: true + transitions: Transition { + to: "checked" + reversible: true + NumberAnimation { + properties: "width" + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutQuad + } } } - onCheckedChanged: { - AppActions.setTaskDone(uid,checked); + // add background to avoid overlapping during animations of surrounding items + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + z: -1 } } - diff --git a/examples/todo/views/TodoList.qml b/examples/todo/views/TodoList.qml index 46ef551..a9aa45f 100644 --- a/examples/todo/views/TodoList.qml +++ b/examples/todo/views/TodoList.qml @@ -1,16 +1,68 @@ import QtQuick 2.0 -import QuickFlux 1.0 -import QtQuick.Controls 1.0 -import QtQuick.Layouts 1.0 -import "../stores" +import org.kde.kirigami 2.0 as Kirigami -ScrollView { - ListView { - anchors.fill: parent +ListView { + id: listView + model: TodoVisualModel {} - model: TodoVisualModel { - id: visualModel + // In theory, animations could be handled with actions, stored etc., + // but it's better to let QtQuick manage it natively. + // + // Cascade effect is taken from Qt docs: + // https://doc.qt.io/qt-5/qml-qtquick-viewtransition.html#animating-items-to-intermediate-positions + + // Slides in right from the above. + add: Transition { + id: addTransition + NumberAnimation { + property: "y" + from: _getPrevItemPosition(addTransition.ViewTransition.index).y + duration: Kirigami.Units.veryLongDuration + easing.type: Easing.OutBounce + } + } + + // Just moves to the new place with animation. + displaced: Transition { + NumberAnimation { + property: "y" + duration: Kirigami.Units.veryLongDuration + easing.type: Easing.OutBounce } } -} + // Swipes out to the right, cascading timing. + remove: Transition { + id: removeTransition + SequentialAnimation { + PauseAnimation { + duration: { + const offset = (removeTransition.ViewTransition.index + - Math.min.apply(null, removeTransition.ViewTransition.targetIndexes)); + const duration = Kirigami.Units.shortDuration / 2; + return offset * duration; + } + } + ParallelAnimation { + NumberAnimation { + property: "x" + to: removeTransition.ViewTransition.item.width + duration: Kirigami.Units.shortDuration + easing.type: Easing.InQuad + } + NumberAnimation { + property: "opacity" + to: 0 + duration: Kirigami.Units.shortDuration + easing.type: Easing.InQuad + } + } + } + } + + function _getPrevItemPosition(index) { + const prevIdx = index - 1; + const prevItem = listView.itemAtIndex(prevIdx); + return listView.mapFromItem(prevItem, 0, 0); + } +} diff --git a/examples/todo/views/TodoVisualModel.qml b/examples/todo/views/TodoVisualModel.qml index 52b11c6..501b9ab 100644 --- a/examples/todo/views/TodoVisualModel.qml +++ b/examples/todo/views/TodoVisualModel.qml @@ -21,10 +21,11 @@ VisualDataModel { uid: model.uid title: model.title checked: model.done + // for animations + z: -index Component.onCompleted: { - item.VisualDataModel.inNonCompleted = Qt.binding(function() { return !model.done}) + item.VisualDataModel.inNonCompleted = Qt.binding(() => !model.done) } } } -