diff --git a/framework/src/domain/todomodel.cpp b/framework/src/domain/todomodel.cpp index e80c4930..f4644007 100644 --- a/framework/src/domain/todomodel.cpp +++ b/framework/src/domain/todomodel.cpp @@ -1,295 +1,267 @@ /* Copyright (c) 2018 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "todomodel.h" #include #include #include #include #include #include #include using namespace Sink; TodoSourceModel::TodoSourceModel(QObject *parent) : QAbstractItemModel(parent), mCalendarCache{EntityCache::Ptr::create()} { mRefreshTimer.setSingleShot(true); QObject::connect(&mRefreshTimer, &QTimer::timeout, this, &TodoSourceModel::updateFromSource); } -void TodoSourceModel::setCalendarFilter(const QSet &calendarFilter) -{ - mCalendarFilter = calendarFilter; - updateQuery(); -} - void TodoSourceModel::setFilter(const QVariantMap &filter) { - mFilter = filter; - updateQuery(); -} - -void TodoSourceModel::updateQuery() -{ + const auto account = filter.value("account").toByteArray(); + const auto calendarFilter = filter.value("calendars").value>().toList(); using namespace Sink::ApplicationDomain; - if (mCalendarFilter.isEmpty()) { + if (calendarFilter.isEmpty()) { refreshView(); return; } Sink::Query query; - // query.resourceFilter(mAccount); + if (!account.isEmpty()) { + query.resourceFilter(account); + } + query.filter(QueryBase::Comparator(QVariant::fromValue(calendarFilter), QueryBase::Comparator::In)); + + if (filter.value("doing").toBool()) { + query.filter("INPROCESS"); + } + query.setFlags(Sink::Query::LiveQuery); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); mSourceModel = Store::loadModel(query); QObject::connect(mSourceModel.data(), &QAbstractItemModel::dataChanged, this, &TodoSourceModel::refreshView); QObject::connect(mSourceModel.data(), &QAbstractItemModel::layoutChanged, this, &TodoSourceModel::refreshView); QObject::connect(mSourceModel.data(), &QAbstractItemModel::modelReset, this, &TodoSourceModel::refreshView); QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsInserted, this, &TodoSourceModel::refreshView); QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsMoved, this, &TodoSourceModel::refreshView); QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &TodoSourceModel::refreshView); refreshView(); } void TodoSourceModel::refreshView() { if (!mRefreshTimer.isActive()) { //Instant update, but then only refresh every 50ms max. updateFromSource(); mRefreshTimer.start(50); } } void TodoSourceModel::updateFromSource() { beginResetModel(); mTodos.clear(); if (mSourceModel) { for (int i = 0; i < mSourceModel->rowCount(); ++i) { auto todo = mSourceModel->index(i, 0).data(Sink::Store::DomainObjectRole).value(); - const bool skip = [&] { - if (!mCalendarFilter.contains(todo->getCalendar())) { - return true; - } - for (auto it = mFilter.constBegin(); it != mFilter.constEnd(); it++) { - if (todo->getProperty(it.key().toLatin1()) != it.value()) { - return true; - } - } - return false; - }(); - if (skip) { - continue; - } - //Parse the todo if(auto icalTodo = KCalCore::ICalFormat().readIncidence(todo->getIcal()).dynamicCast()) { mTodos.append({icalTodo->dtStart(), icalTodo->dtDue(), icalTodo->completed(), icalTodo, getColor(todo->getCalendar()), todo->getStatus(), todo, todo->getPriority()}); } else { SinkWarning() << "Invalid ICal to process, ignoring..."; } } } endResetModel(); } QModelIndex TodoSourceModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return {}; } if (!parent.isValid()) { return createIndex(row, column); } return {}; } QModelIndex TodoSourceModel::parent(const QModelIndex &) const { return {}; } int TodoSourceModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return mTodos.size(); } return 0; } int TodoSourceModel::columnCount(const QModelIndex &) const { return 1; } QByteArray TodoSourceModel::getColor(const QByteArray &calendar) const { const auto color = mCalendarCache->getProperty(calendar, "color").toByteArray(); if (color.isEmpty()) { qWarning() << "Failed to get color for calendar " << calendar; } return color; } QVariant TodoSourceModel::data(const QModelIndex &idx, int role) const { if (!hasIndex(idx.row(), idx.column())) { return {}; } auto todo = mTodos.at(idx.row()); auto icalTodo = todo.incidence; switch (role) { case Summary: return icalTodo->summary(); case Description: return icalTodo->description(); case StartDate: return todo.start; case DueDate: return todo.due; case Date: if (todo.status == "COMPLETED") { return todo.completed; } if (todo.due.isValid()) { return todo.due; } return todo.start; case CompletedDate: return todo.completed; case Color: return todo.color; case Status: return todo.status; case Complete: return todo.status == "COMPLETED"; case Doing: return todo.status == "INPROCESS"; case Important: return todo.priority == 1; case Relevance: { int score = 100; if (todo.status == "COMPLETED") { score -= 100; } else { //TODO add if due date is soon //TODO add more if overdue if (todo.priority == 1) { score += 50; } if (todo.status == "INPROCESS") { score += 100; } } return score; } case Todo: return QVariant::fromValue(todo.domainObject); default: SinkWarning() << "Unknown role for todo:" << QMetaEnum::fromType().valueToKey(role) << role; return {}; } } QHash TodoSourceModel::roleNames() const { return { {Summary, "summary"}, {Description, "description"}, {StartDate, "startDate"}, {DueDate, "dueDate"}, {CompletedDate, "completedDate"}, {Date, "date"}, {Color, "color"}, {Status, "status"}, {Complete, "complete"}, {Doing, "doing"}, {Important, "important"}, {Todo, "domainObject"} }; } TodoModel::TodoModel(QObject *parent) : QSortFilterProxyModel(parent) { setDynamicSortFilter(true); sort(0, Qt::DescendingOrder); setFilterCaseSensitivity(Qt::CaseInsensitive); setSourceModel(new TodoSourceModel(this)); } QHash TodoModel::roleNames() const { return sourceModel()->roleNames(); } -void TodoModel::setCalendarFilter(const QSet &filter) -{ - static_cast(sourceModel())->setCalendarFilter(filter); -} - void TodoModel::setFilter(const QVariantMap &f) { - auto filter = f; - if (filter.contains("doing")) { - if (filter.take("doing").toBool()) { - filter.insert("status", "INPROCESS"); - } - } - static_cast(sourceModel())->setFilter(filter); + static_cast(sourceModel())->setFilter(f); } bool TodoModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto leftScore = left.data(TodoSourceModel::Relevance).toInt(); const auto rightScore = right.data(TodoSourceModel::Relevance).toInt(); if (leftScore == rightScore) { return left.data(TodoSourceModel::Summary) < right.data(TodoSourceModel::Summary); } return leftScore < rightScore; } bool TodoModel::filterAcceptsRow(int /*sourceRow*/, const QModelIndex &/*sourceParent*/) const { return true; } diff --git a/framework/src/domain/todomodel.h b/framework/src/domain/todomodel.h index 33da5018..aec68ddb 100644 --- a/framework/src/domain/todomodel.h +++ b/framework/src/domain/todomodel.h @@ -1,128 +1,118 @@ /* Copyright (c) 2018 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #pragma once #include "kube_export.h" #include #include #include #include #include #include #include namespace KCalCore { class Incidence; } namespace Sink { namespace ApplicationDomain { struct Todo; } } class EntityCacheInterface; class KUBE_EXPORT TodoSourceModel : public QAbstractItemModel { Q_OBJECT public: enum Roles { Summary = Qt::UserRole + 1, Description, StartDate, DueDate, CompletedDate, Date, Color, Status, Complete, Doing, Important, Relevance, Todo, LastRole }; Q_ENUM(Roles); TodoSourceModel(QObject *parent = nullptr); ~TodoSourceModel() = default; QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; - void updateQuery(const QDate &start, const QDate &end, const QSet &calendarFilter); - - void setCalendarFilter(const QSet &); void setFilter(const QVariantMap &); - private: - void updateQuery(); - void refreshView(); void updateFromSource(); QByteArray getColor(const QByteArray &calendar) const; QSharedPointer mSourceModel; QSet mCalendarFilter; QSharedPointer mCalendarCache; QTimer mRefreshTimer; struct Occurrence { QDateTime start; QDateTime due; QDateTime completed; QSharedPointer incidence; QByteArray color; QString status; QSharedPointer domainObject; int priority; }; QList mTodos; - QVariantMap mFilter; - QByteArray mAccount; }; class KUBE_EXPORT TodoModel : public QSortFilterProxyModel { Q_OBJECT - Q_PROPERTY(QSet calendarFilter WRITE setCalendarFilter) Q_PROPERTY(QVariantMap filter WRITE setFilter) public: TodoModel(QObject *parent = nullptr); ~TodoModel() = default; QHash roleNames() const override; - void setCalendarFilter(const QSet &); void setFilter(const QVariantMap &); protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; }; diff --git a/views/todo/qml/View.qml b/views/todo/qml/View.qml index d1b3bace..c56c14d9 100644 --- a/views/todo/qml/View.qml +++ b/views/todo/qml/View.qml @@ -1,401 +1,405 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.9 import QtQuick.Controls 1.3 as Controls1 import QtQuick.Controls 2 import QtQuick.Layouts 1.1 import org.kube.framework 1.0 as Kube Kube.View { id: root property alias currentAccount: accountSwitcher.currentAccount // property variant currentFolder: null + property bool doing: true //We have to hardcode because all the mapToItem/mapFromItem functions are garbage // searchArea: Qt.rect(ApplicationWindow.window.sidebarWidth + mailListView.parent.x, 0, (mailView.x + mailView.width) - mailListView.parent.x, (mailView.y + mailView.height) - mailListView.y) onFilterChanged: { Kube.Fabric.postMessage(Kube.Messages.searchString, {"searchString": filter}) } onRefresh: { // if (!!root.currentFolder) { // Kube.Fabric.postMessage(Kube.Messages.synchronize, {"folder": root.currentFolder}); // Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": root.currentAccount, "type": "folder"}) // } else { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": root.currentAccount}) // } } Kube.Listener { filter: Kube.Messages.search onMessageReceived: root.triggerSearch() } helpViewComponent: Kube.HelpPopup { ListModel { ListElement { description: qsTr("Go to next todo:"); shortcut: "j" } ListElement { description: qsTr("Go to previous todo:"); shortcut: "k" } ListElement { description: qsTr("Create new todo:"); shortcut: "c" } ListElement { description: qsTr("Show this help text:"); shortcut: "?" } } } Shortcut { sequences: ['j'] onActivated: todoView.incrementCurrentIndex() } Shortcut { sequences: ['k'] onActivated: todoView.decrementCurrentIndex() } Shortcut { sequences: ['c'] onActivated: editorPopup.createObject(root, {}).open() } Shortcut { enabled: root.isCurrentView sequence: "?" onActivated: root.showHelp() } ButtonGroup { id: viewButtonGroup } Controls1.SplitView { Layout.fillWidth: true Layout.fillHeight: true Rectangle { width: Kube.Units.gridUnit * 10 Layout.fillHeight: parent.height color: Kube.Colors.darkBackgroundColor Column { id: topLayout anchors { top: parent.top left: parent.left right: parent.right margins: Kube.Units.largeSpacing } Kube.PositiveButton { id: newMailButton objectName: "newMailButton" anchors { left: parent.left right: parent.right } focus: true text: qsTr("New Todo") onClicked: editorPopup.createObject(root, {}).open() } Item { anchors { left: parent.left right: parent.right } height: Kube.Units.gridUnit } Kube.TextButton { id: doingViewButton anchors { left: parent.left right: parent.right } text: qsTr("Doing") textColor: Kube.Colors.highlightedTextColor checkable: true - checked: true + checked: root.doing horizontalAlignment: Text.AlignHLeft ButtonGroup.group: viewButtonGroup - onClicked: todoModel.filter = {"doing": true} + onClicked: root.doing = true } Kube.TextButton { id: allViewButton anchors { left: parent.left right: parent.right } text: qsTr("All") textColor: Kube.Colors.highlightedTextColor checkable: true horizontalAlignment: Text.AlignHLeft ButtonGroup.group: viewButtonGroup - onClicked: todoModel.filter = {} + onClicked: root.doing = false } } Kube.CalendarSelector { id: accountSwitcher activeFocusOnTab: true anchors { top: topLayout.bottom topMargin: Kube.Units.largeSpacing bottom: statusBarContainer.top left: topLayout.left right: parent.right rightMargin: Kube.Units.largeSpacing } contentType: "todo" } Item { id: statusBarContainer anchors { topMargin: Kube.Units.smallSpacing bottom: parent.bottom left: parent.left right: parent.right } height: childrenRect.height Rectangle { id: border visible: statusBar.visible anchors { right: parent.right left: parent.left margins: Kube.Units.smallSpacing } height: 1 color: Kube.Colors.viewBackgroundColor opacity: 0.3 } Kube.StatusBar { id: statusBar accountId: root.currentAccount height: Kube.Units.gridUnit * 2 anchors { top: border.bottom left: statusBarContainer.left right: statusBarContainer.right } } } } Rectangle { width: Kube.Units.gridUnit * 18 Layout.fillHeight: parent.height color: "transparent" border.width: 1 border.color: Kube.Colors.buttonColor Kube.ListView { id: todoView anchors.fill: parent Layout.minimumWidth: Kube.Units.gridUnit * 10 onCurrentItemChanged: { if (currentItem) { var currentData = currentItem.currentData; todoDetails.controller = controllerComponent.createObject(parent, {"todo": currentData.domainObject}) } } Column { anchors.centerIn: parent visible: todoView.count === 0 Kube.Label { text: qsTr("Nothing here yet...") } Kube.PositiveButton { visible: doingViewButton.checked text: qsTr("Pick some tasks") onClicked: { allViewButton.checked = true allViewButton.clicked() } } Kube.PositiveButton { visible: allViewButton.checked text: qsTr("Add a new task") onClicked: editorPopup.createObject(root, {}).open() } } model: Kube.TodoModel { id: todoModel - calendarFilter: accountSwitcher.enabledCalendars - filter: {"doing": true} + filter: { + "account": accountSwitcher.currentAccount, + "calendars": accountSwitcher.enabledCalendars, + "doing": root.doing, + } } delegate: Kube.ListDelegate { id: delegateRoot //Required for D&D // property var mail: model.mail property bool buttonsVisible: delegateRoot.hovered width: todoView.availableWidth height: Kube.Units.gridUnit * 3 color: Kube.Colors.viewBackgroundColor border.color: Kube.Colors.backgroundColor border.width: 1 Item { id: content anchors { fill: parent margins: Kube.Units.smallSpacing } Column { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: Kube.Units.largeSpacing } Kube.Label{ id: subject width: content.width - Kube.Units.gridUnit * 3 text: model.summary color: delegateRoot.textColor font.strikeout: model.complete font.bold: model.doing && allViewButton.checked maximumLineCount: 2 wrapMode: Text.WordWrap elide: Text.ElideRight } } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom } visible: model.date && !delegateRoot.buttonsVisible text: Qt.formatDateTime(model.date, "dd MMM yyyy") font.italic: true color: delegateRoot.disabledTextColor font.pointSize: Kube.Units.tinyFontSize } } Kube.Icon { anchors { right: parent.right verticalCenter: parent.verticalCenter margins: Kube.Units.smallSpacing } visible: model.important && !delegateRoot.buttonsVisible iconName: Kube.Icons.isImportant } Column { id: buttons anchors { right: parent.right margins: Kube.Units.smallSpacing verticalCenter: parent.verticalCenter } visible: delegateRoot.buttonsVisible opacity: 0.7 Kube.IconButton { iconName: model.doing ? Kube.Icons.listRemove : Kube.Icons.addNew activeFocusOnTab: false tooltip: model.doing ? qsTr("Unpick") : qsTr("Pick") onClicked: { var controller = controllerComponent.createObject(parent, {"todo": model.domainObject}); if (controller.complete) { controller.complete = false } controller.doing = !controller.doing; controller.saveAction.execute(); } } Kube.IconButton { iconName: Kube.Icons.checkbox checked: model.complete activeFocusOnTab: false tooltip: qsTr("Done!") onClicked: { var controller = controllerComponent.createObject(parent, {"todo": model.domainObject}); controller.complete = !controller.complete; controller.saveAction.execute(); } } } } } } Rectangle { Layout.fillHeight: parent.height Layout.fillWidth: true color: "transparent" border.width: 1 border.color: Kube.Colors.buttonColor TodoView { id: todoDetails anchors.fill: parent // onDone: popup.close() } } Component { id: controllerComponent Kube.TodoController { } } } Kube.Listener { filter: Kube.Messages.eventEditor onMessageReceived: eventPopup.createObject(root, message).open() } Component { id: editorPopup Kube.Popup { id: popup x: root.width * 0.15 y: root.height * 0.15 width: root.width * 0.7 height: root.height * 0.7 padding: 0 TodoEditor { id: editor anchors.fill: parent accountId: root.currentAccount doing: doingViewButton.checked onDone: popup.close() } } } }