diff --git a/framework/qml/InlineAccountSwitcher.qml b/framework/qml/InlineAccountSwitcher.qml index 759bfb02..86e67365 100644 --- a/framework/qml/InlineAccountSwitcher.qml +++ b/framework/qml/InlineAccountSwitcher.qml @@ -1,108 +1,108 @@ /* * Copyright (C) 2017 Michael Bohlender, * * 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.4 import QtQuick.Layouts 1.1 import org.kube.framework 1.0 as Kube FocusScope { id: root property string currentAccount property string _currentAccount: Kube.Context.currentAccountId property Component delegate: null onCurrentAccountChanged: { Kube.Context.currentAccountId = currentAccount } ColumnLayout { id: layout anchors.fill: parent Repeater { model: Kube.AccountsModel {} onItemAdded: { //Autoselect the first account to appear if (!_currentAccount) { root.currentAccount = item.currentData.accountId } } delegate: ColumnLayout { id: accountDelegate property variant currentData: model property bool isCurrent: (model.accountId == root._currentAccount) Layout.minimumHeight: Kube.Units.gridUnit Layout.fillHeight: isCurrent Layout.fillWidth: true Item { Layout.fillWidth: true Layout.preferredHeight: Kube.Units.gridUnit Kube.TextButton { anchors { left: parent.left top: parent.top } height: Kube.Units.gridUnit width: parent.width - Kube.Units.gridUnit textColor: Kube.Colors.highlightedTextColor activeFocusOnTab: !isCurrent hoverEnabled: !isCurrent onClicked: root.currentAccount = model.accountId text: model.name font.weight: Font.Bold font.family: Kube.Font.fontFamily horizontalAlignment: Text.AlignHLeft padding: 0 } Loader { anchors { right: parent.right top: parent.top } focus: isCurrent activeFocusOnTab: isCurrent visible: isCurrent sourceComponent: delegateLoader.item.buttonDelegate } } Loader { id: delegateLoader Layout.fillWidth: true Layout.fillHeight: true focus: accountDelegate.isCurrent activeFocusOnTab: true visible: accountDelegate.isCurrent sourceComponent: root.delegate property variant accountId: currentData.accountId property bool isCurrent: accountDelegate.isCurrent onIsCurrentChanged: { - if (item.currentChanged) { + if (!!item && 'currentChanged' in item) { item.currentChanged() } } } } } } } diff --git a/framework/qml/TreeView.qml b/framework/qml/TreeView.qml index 939f1401..585ccbdc 100644 --- a/framework/qml/TreeView.qml +++ b/framework/qml/TreeView.qml @@ -1,113 +1,116 @@ /* Copyright (C) 2016 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 import QtQuick.Controls 2 import QtQuick.Layouts 1 import org.kube.framework 1.0 as Kube FocusScope { id: root property var model: null - readonly property var currentIndex: modelAdaptor.mapRowToModelIndex(listView.currentIndex) + property var currentIndex: modelAdaptor.mapRowToModelIndex(listView.currentIndex) + property alias count: listView.count function selectRootIndex() { - listView.currentIndex = 0 + if (listView.count >= 1) { + listView.currentIndex = 0 + } } function selectNext() { listView.incrementCurrentIndex() } function selectPrevious() { listView.decrementCurrentIndex() } Kube.ListView { id: listView anchors.fill: parent focus: true model: Kube.TreeModelAdaptor { id: modelAdaptor model: root.model } ScrollBar.vertical: Kube.ScrollBar { invertedColors: true } delegate: Kube.ListDelegate { id: delegate width: listView.availableWidth height: Kube.Units.gridUnit * 1.5 hoverEnabled: true property bool isActive: listView.currentIndex === index background: Kube.DelegateBackground { anchors.fill: parent color: Kube.Colors.textColor focused: delegate.activeFocus || delegate.hovered selected: isActive } function toggleExpanded() { var idx = model._q_TreeView_ModelIndex if (modelAdaptor.isExpanded(idx)) { modelAdaptor.collapse(idx) } else { modelAdaptor.expand(idx) } } Keys.onSpacePressed: toggleExpanded() RowLayout { anchors { fill: parent leftMargin: 2 + (model._q_TreeView_ItemDepth + 1) * Kube.Units.largeSpacing } spacing: Kube.Units.smallSpacing Kube.Label { id: label Layout.fillWidth: true text: model.name color: Kube.Colors.highlightedTextColor elide: Text.ElideLeft clip: false Kube.IconButton { anchors { right: label.left verticalCenter: label.verticalCenter } visible: model._q_TreeView_HasChildren iconName: model._q_TreeView_ItemExpanded ? Kube.Icons.goDown_inverted : Kube.Icons.goNext_inverted padding: 0 width: Kube.Units.gridUnit height: Kube.Units.gridUnit onClicked: toggleExpanded() activeFocusOnTab: false hoverEnabled: false } } } } } } diff --git a/framework/src/domain/folderlistmodel.cpp b/framework/src/domain/folderlistmodel.cpp index 208819af..a7ca8dfa 100644 --- a/framework/src/domain/folderlistmodel.cpp +++ b/framework/src/domain/folderlistmodel.cpp @@ -1,237 +1,240 @@ /* Copyright (c) 2016 Michael Bohlender Copyright (c) 2016 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 "folderlistmodel.h" #include #include #include #include #include using namespace Sink; using namespace Sink::ApplicationDomain; FolderListModel::FolderListModel(QObject *parent) : KRecursiveFilterProxyModel(parent) { setDynamicSortFilter(true); sort(0, Qt::AscendingOrder); //Automatically fetch all folders, otherwise the recursive filtering does not work. QObject::connect(this, &QSortFilterProxyModel::sourceModelChanged, [this] () { if (sourceModel()) { QObject::connect(sourceModel(), &QAbstractItemModel::rowsInserted, sourceModel(), [this] (QModelIndex parent, int first, int last) { for (int row = first; row <= last; row++) { auto idx = sourceModel()->index(row, 0, parent); sourceModel()->fetchMore(idx); } }); - QObject::connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, const QVector &roles) { - if (roles.contains(Sink::Store::ChildrenFetchedRole)) { - emit initialItemsLoaded(); - } - }); } }); } FolderListModel::~FolderListModel() { } QHash< int, QByteArray > FolderListModel::roleNames() const { QHash roles; roles[Name] = "name"; roles[Icon] = "icon"; roles[Id] = "id"; roles[DomainObject] = "domainObject"; roles[Status] = "status"; roles[Trash] = "trash"; roles[HasNewData] = "hasNewData"; return roles; } QVariant FolderListModel::data(const QModelIndex &idx, int role) const { auto srcIdx = mapToSource(idx); auto folder = srcIdx.data(Sink::Store::DomainObjectRole).value(); switch (role) { case Name: return folder->getName(); case Icon: return folder->getIcon(); case Id: return folder->identifier(); case DomainObject: return QVariant::fromValue(folder); case Status: { switch (srcIdx.data(Sink::Store::StatusRole).toInt()) { case Sink::ApplicationDomain::SyncStatus::SyncInProgress: return InProgressStatus; case Sink::ApplicationDomain::SyncStatus::SyncError: return ErrorStatus; case Sink::ApplicationDomain::SyncStatus::SyncSuccess: return SuccessStatus; } return NoStatus; } case Trash: if (folder) { return folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash); } return false; case HasNewData: return mHasNewData.contains(folder->identifier()); } return QSortFilterProxyModel::data(idx, role); } static QModelIndex findRecursive(QAbstractItemModel *model, const QModelIndex &parent, int role, const QVariant &value) { for (auto row = 0; row < model->rowCount(parent); row++) { const auto idx = model->index(row, 0, parent); if (model->data(idx, role) == value) { return idx; } auto result = findRecursive(model, idx, role, value); if (result.isValid()) { return result; } } return {}; } void FolderListModel::runQuery(const Query &query) { mModel = Store::loadModel(query); + QObject::connect(mModel.data(), &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, const QVector &roles) { + if (roles.contains(Sink::Store::ChildrenFetchedRole)) { + emit initialItemsLoaded(); + } + }); setSourceModel(mModel.data()); + if (!mModel->canFetchMore({})) { + emit initialItemsLoaded(); + } Sink::Query resourceQuery; resourceQuery.setFilter(query.getResourceFilter()); mNotifier.reset(new Sink::Notifier{resourceQuery}); mNotifier->registerHandler([&](const Sink::Notification ¬ification) { if (notification.type == Sink::Notification::Info && notification.code == ApplicationDomain::NewContentAvailable) { if (!notification.entities.isEmpty()) { mHasNewData.insert(notification.entities.first()); auto idx = findRecursive(this, {}, Id, QVariant::fromValue(notification.entities.first())); if (idx.isValid()) { emit dataChanged(idx, idx); } } } }); } void FolderListModel::setAccountId(const QVariant &accountId) { const auto account = accountId.toString().toUtf8(); //Get all folders of an account auto query = Query(); query.resourceFilter(account); query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus); query.request() .request() .request() .request() .request(); query.requestTree(); query.setId("foldertree" + account); runQuery(query); } QVariant FolderListModel::accountId() const { return {}; } static int getPriority(const Sink::ApplicationDomain::Folder &folder) { auto specialPurpose = folder.getSpecialPurpose(); if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::inbox)) { return 5; } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::drafts)) { return 6; } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::sent)) { return 7; } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash)) { return 8; } else if (!specialPurpose.isEmpty()) { return 9; } return 10; } bool FolderListModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto leftFolder = left.data(Sink::Store::DomainObjectRole).value(); const auto rightFolder = right.data(Sink::Store::DomainObjectRole).value(); const auto leftPriority = getPriority(*leftFolder); const auto rightPriority = getPriority(*rightFolder); if (leftPriority == rightPriority) { return leftFolder->getName() < rightFolder->getName(); } return leftPriority < rightPriority; } bool FolderListModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const { auto index = sourceModel()->index(sourceRow, 0, sourceParent); Q_ASSERT(index.isValid()); const auto folder = index.data(Sink::Store::DomainObjectRole).value(); Q_ASSERT(folder); const auto enabled = folder->getEnabled(); return enabled; } void FolderListModel::fetchMore(const QModelIndex &parent) { mHasNewData.remove(parent.data(Id).toByteArray()); QAbstractItemModel::fetchMore(parent); } void FolderListModel::setFolderId(const QVariant &folderId) { const auto folder = folderId.toString().toUtf8(); if (folder.isEmpty()) { setSourceModel(nullptr); mModel.clear(); return; } //Get all folders of an account auto query = Query(); query.filter(folder); query.request() .request() .request() .request() .request(); query.setId("folder" + folder); runQuery(query); } QVariant FolderListModel::folderId() const { return {}; } diff --git a/views/conversation/qml/View.qml b/views/conversation/qml/View.qml index 2bc0e81d..04471d54 100644 --- a/views/conversation/qml/View.qml +++ b/views/conversation/qml/View.qml @@ -1,273 +1,274 @@ /* * 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 variant currentFolder: null //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: { mailListView.filter = filter 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": Kube.Context.currentAccountId, "type": "folder"}) } else { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": Kube.Context.currentAccountId}) } } Kube.Listener { filter: Kube.Messages.search onMessageReceived: root.triggerSearch() } helpViewComponent: Kube.HelpPopup { ListModel { ListElement { description: qsTr("Jump to next thread:"); shortcut: "j" } ListElement { description: qsTr("Jump to previous thread:"); shortcut: "k" } ListElement { description: qsTr("Jump to next message:"); shortcut: "n" } ListElement { description: qsTr("Jump to previous message:"); shortcut: "p" } ListElement { description: qsTr("Jump to next folder:"); shortcut: "f,n" } ListElement { description: qsTr("Jump to previous previous folder:"); shortcut: "f,p" } ListElement { description: qsTr("Compose new message:"); shortcut: "c" } ListElement { description: qsTr("Reply to the currently focused message:"); shortcut: "r" } ListElement { description: qsTr("Delete the currently focused message:"); shortcut: "d" } ListElement { description: qsTr("Mark the currently focused message as important:"); shortcut: "i" } ListElement { description: qsTr("Mark the currently focused message as unread:"); shortcut: "u" } ListElement { description: qsTr("Show this help text:"); shortcut: "?" } } } Shortcut { enabled: root.isCurrentView sequences: ['j'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectNextConversation, {}) } Shortcut { enabled: root.isCurrentView sequences: ['k'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectPreviousConversation, {}) } Shortcut { enabled: root.isCurrentView sequences: ['Shift+J'] onActivated: Kube.Fabric.postMessage(Kube.Messages.scrollConversationDown, {}) } Shortcut { enabled: root.isCurrentView sequences: ['Shift+K'] onActivated: Kube.Fabric.postMessage(Kube.Messages.scrollConversationUp, {}) } Shortcut { sequences: ['n'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectNextMessage, {}) } Shortcut { enabled: root.isCurrentView sequences: ['p'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectPreviousMessage, {}) } Shortcut { enabled: root.isCurrentView sequences: ['f,n'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectNextFolder, {}) } Shortcut { enabled: root.isCurrentView sequences: ['f,p'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectPreviousFolder, {}) } Shortcut { enabled: root.isCurrentView sequences: ['c'] onActivated: Kube.Fabric.postMessage(Kube.Messages.compose, {}) } Shortcut { enabled: root.isCurrentView sequence: "?" onActivated: root.showHelp() } Controls1.SplitView { Layout.fillWidth: true Layout.fillHeight: true Rectangle { width: Kube.Units.gridUnit * 10 Layout.fillHeight: parent.height color: Kube.Colors.darkBackgroundColor Kube.PositiveButton { id: newMailButton objectName: "newMailButton" anchors { top: parent.top left: parent.left right: parent.right margins: Kube.Units.largeSpacing } focus: true text: qsTr("New Email") onClicked: Kube.Fabric.postMessage(Kube.Messages.compose, {}) } Kube.InlineAccountSwitcher { id: accountFolderview activeFocusOnTab: true anchors { top: newMailButton.bottom topMargin: Kube.Units.largeSpacing bottom: statusBarContainer.top left: newMailButton.left right: parent.right } delegate: Kube.FolderListView { objectName: "folderListView" accountId: parent.accountId function indexSelected(currentIndex) { - Kube.Fabric.postMessage(Kube.Messages.folderSelection, {"folder": model.data(currentIndex, Kube.FolderListModel.DomainObject), + var folder = model.data(currentIndex, Kube.FolderListModel.DomainObject) + Kube.Fabric.postMessage(Kube.Messages.folderSelection, {"folder": folder, "trash": model.data(currentIndex, Kube.FolderListModel.Trash)}) - root.currentFolder = model.data(currentIndex, Kube.FolderListModel.DomainObject) + root.currentFolder = folder } //Necessary to re-select on account change function currentChanged() { if (isCurrent) { indexSelected(currentIndex) } } onCurrentIndexChanged: { if (isCurrent) { indexSelected(currentIndex) } } } } 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: Kube.Context.currentAccountId 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.MailListView { id: mailListView objectName: "mailListView" anchors.fill: parent activeFocusOnTab: true Layout.minimumWidth: Kube.Units.gridUnit * 10 Kube.Listener { filter: Kube.Messages.folderSelection onMessageReceived: { root.clearSearch() mailListView.parentFolder = message.folder } } onCurrentMailChanged: { Kube.Fabric.postMessage(Kube.Messages.mailSelection, {"mail": currentMail}) } } } Kube.ConversationView { id: mailView objectName: "mailView" Layout.fillWidth: true Layout.fillHeight: parent.height activeFocusOnTab: true model: Kube.MailListModel { id: mailViewModel } Kube.Listener { filter: Kube.Messages.mailSelection onMessageReceived: { if (!mailListView.threaded) { mailViewModel.singleMail = message.mail } else { mailViewModel.mail = message.mail } } } Kube.Listener { filter: Kube.Messages.folderSelection onMessageReceived: { mailView.hideTrash = !message.trash mailView.hideNonTrash = message.trash } } } } }