diff --git a/framework/qml/MailListView.qml b/framework/qml/MailListView.qml index 9a383178..fe0933a7 100644 --- a/framework/qml/MailListView.qml +++ b/framework/qml/MailListView.qml @@ -1,329 +1,331 @@ /* Copyright (C) 2016 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.9 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.1 import org.kube.framework 1.0 as Kube FocusScope { id: root //Private properties property var parentFolder: null property var currentMail: null + property var currentAccount: null property bool showImportant: false property string filter: "" property bool threaded: false onParentFolderChanged: { currentMail = null } Kube.Listener { filter: Kube.Messages.selectTopConversation onMessageReceived: { listView.currentIndex = 0 listView.forceActiveFocus() } } Kube.Listener { filter: Kube.Messages.selectNextConversation onMessageReceived: { listView.incrementCurrentIndex() listView.forceActiveFocus() } } Kube.Listener { filter: Kube.Messages.selectPreviousConversation onMessageReceived: { listView.decrementCurrentIndex() listView.forceActiveFocus() } } Kube.Label { anchors.centerIn: parent visible: listView.count === 0 //TODO depending on whether we synchronized already or not the label should change. text: qsTr("Nothing here...") } ColumnLayout { anchors.fill: parent spacing: 0 Kube.ListView { id: listView objectName: "listView" Layout.fillWidth: true Layout.fillHeight: true clip: true focus: true onActiveFocusChanged: { if (activeFocus && currentIndex < 0) { currentIndex = 0 } } Keys.onPressed: { //Not implemented as a shortcut because we want it only to apply if we have the focus if (event.text == "d" || event.key == Qt.Key_Delete) { Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": root.currentMail}) } else if (event.text == "r") { Kube.Fabric.postMessage(Kube.Messages.reply, {"mail": root.currentMail}) } else if (event.text == "i") { Kube.Fabric.postMessage(Kube.Messages.setImportant, {"mail": root.currentMail, "important": !currentItem.currentData.important}) } else if (event.text == "u") { Kube.Fabric.postMessage(Kube.Messages.markAsUnread, {"mail": root.currentMail}) } if (event.key == Qt.Key_Home) { listView.currentIndex = 0 } } onCurrentItemChanged: { if (currentItem) { var currentData = currentItem.currentData; root.currentMail = currentData.mail; if (currentData.mail && currentData.unread) { Kube.Fabric.postMessage(Kube.Messages.markAsRead, {"mail": currentData.mail}) } } } model: Kube.MailListModel { id: mailListModel filter: { "folder": root.parentFolder, "important": root.showImportant, "string": root.filter, - "threaded": root.threaded + "threaded": root.threaded, + "account": root.currentAccount, } } delegate: Kube.ListDelegate { id: delegateRoot //Required for D&D property var mail: model.mail property bool buttonsVisible: delegateRoot.hovered width: listView.availableWidth height: Kube.Units.gridUnit * 5 color: Kube.Colors.viewBackgroundColor border.color: Kube.Colors.backgroundColor border.width: 1 states: [ State { name: "dnd" when: mouseArea.drag.active PropertyChanges {target: mouseArea; cursorShape: Qt.ClosedHandCursor} PropertyChanges {target: delegateRoot; x: x; y: y} PropertyChanges {target: delegateRoot; parent: root} PropertyChanges {target: delegateRoot; opacity: 0.2} PropertyChanges {target: delegateRoot; highlighted: true} } ] Drag.active: mouseArea.drag.active Drag.hotSpot.x: mouseArea.mouseX Drag.hotSpot.y: mouseArea.mouseY Drag.source: delegateRoot MouseArea { id: mouseArea anchors.fill: parent drag.target: parent onReleased: { var dropAction = parent.Drag.drop() if (dropAction == Qt.MoveAction) { parent.visible = false } } onClicked: delegateRoot.clicked() } Item { id: content anchors { fill: parent margins: Kube.Units.smallSpacing } property color unreadColor: (model.unread && !delegateRoot.highlighted) ? Kube.Colors.highlightColor : delegateRoot.textColor //TODO batch editing // Kube.CheckBox { // id: checkBox // // anchors.verticalCenter: parent.verticalCenter // visible: (checked || delegateRoot.hovered) && !mouseArea.drag.active // opacity: 0.9 // } Column { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: Kube.Units.largeSpacing // + checkBox.width } Kube.Label{ id: subject width: content.width - Kube.Units.gridUnit * 3 text: model.subject color: content.unreadColor maximumLineCount: 2 wrapMode: Text.WordWrap elide: Text.ElideRight } Kube.Label { id: sender text: model.senderName color: delegateRoot.textColor font.italic: true width: delegateRoot.width - Kube.Units.gridUnit * 3 elide: Text.ElideRight } } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom } function sameDay(date1, date2) { return date1.getFullYear() == date2.getFullYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate() } function formatDateTime(date) { const today = new Date() if (sameDay(date, today)) { return Qt.formatDateTime(date, "hh:mm") } const lastWeekToday = today.getTime() - ((24*60*60*1000) * 7); if (date.getTime() >= lastWeekToday) { return Qt.formatDateTime(date, "ddd hh:mm") } return Qt.formatDateTime(date, "dd MMM yyyy") } visible: !delegateRoot.buttonsVisible text: formatDateTime(model.date) font.italic: true color: delegateRoot.disabledTextColor font.pointSize: Kube.Units.tinyFontSize } Kube.Label { id: threadCounter anchors { right: parent.right margins: Kube.Units.smallSpacing } text: model.threadSize color: content.unreadColor visible: model.threadSize > 1 && !delegateRoot.buttonsVisible } } Kube.Icon { anchors { right: parent.right verticalCenter: parent.verticalCenter margins: Kube.Units.smallSpacing } visible: model.important && !delegateRoot.buttonsVisible && !mouseArea.drag.active iconName: Kube.Icons.isImportant } Column { id: buttons anchors { right: parent.right margins: Kube.Units.smallSpacing verticalCenter: parent.verticalCenter } visible: delegateRoot.buttonsVisible && !mouseArea.drag.active opacity: 0.7 Kube.IconButton { id: restoreButton iconName: Kube.Icons.undo visible: !!model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.restoreFromTrash, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Restore from trash") } Kube.IconButton { id: readButton iconName: Kube.Icons.markAsRead visible: model.unread && !model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.markAsRead, {"mail": model.mail}) tooltip: qsTr("Mark as read") } Kube.IconButton { id: unreadButton iconName: Kube.Icons.markAsUnread visible: !model.unread && !model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.markAsUnread, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Mark as unread") } Kube.IconButton { id: importantButton iconName: model.important ? Kube.Icons.markImportant : Kube.Icons.markUnimportant visible: !!model.mail onClicked: Kube.Fabric.postMessage(Kube.Messages.setImportant, {"mail": model.mail, "important": !model.important}) activeFocusOnTab: false tooltip: qsTr("Mark as important") } Kube.IconButton { id: deleteButton objectName: "deleteButton" iconName: Kube.Icons.moveToTrash visible: !!model.mail onClicked: Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Move to trash") } } } } } } diff --git a/framework/src/domain/maillistmodel.cpp b/framework/src/domain/maillistmodel.cpp index 870ad3d0..ec1a25a5 100644 --- a/framework/src/domain/maillistmodel.cpp +++ b/framework/src/domain/maillistmodel.cpp @@ -1,472 +1,490 @@ /* 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 "maillistmodel.h" #include MailListModel::MailListModel(QObject *parent) : QSortFilterProxyModel(parent) { setDynamicSortFilter(true); sort(0, Qt::DescendingOrder); setFilterCaseSensitivity(Qt::CaseInsensitive); } MailListModel::~MailListModel() { } static void requestHeaders(Sink::Query &query) { using namespace Sink::ApplicationDomain; query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); } static void requestFullMail(Sink::Query &query) { using namespace Sink::ApplicationDomain; requestHeaders(query); query.request(); query.request(); } void MailListModel::setFilterString(const QString &filter) { if (filter.length() < 3 && !filter.isEmpty()) { return; } auto oldQuery = mQuery; auto query = mQuery; if (!filter.isEmpty()) { //Avoid live updates until we properly filter updates query.setFlags(Sink::Query::NoFlags); auto f = filter; if (mCurrentQueryItem.isEmpty()) { requestHeaders(query); } query.filter({}, Sink::QueryBase::Comparator(f, Sink::QueryBase::Comparator::Fulltext)); query.limit(0); } runQuery(query); mQuery = oldQuery; } QString MailListModel::filterString() const { return {}; } QHash< int, QByteArray > MailListModel::roleNames() const { QHash roles; roles[Subject] = "subject"; roles[Sender] = "sender"; roles[SenderName] = "senderName"; roles[To] = "to"; roles[Cc] = "cc"; roles[Bcc] = "bcc"; roles[Date] = "date"; roles[Unread] = "unread"; roles[Important] = "important"; roles[Draft] = "draft"; roles[Sent] = "sent"; roles[Trash] = "trash"; roles[Id] = "id"; roles[MimeMessage] = "mimeMessage"; roles[DomainObject] = "domainObject"; roles[ThreadSize] = "threadSize"; roles[Mail] = "mail"; roles[Incomplete] = "incomplete"; roles[Status] = "status"; return roles; } static QString join(const QList &contacts) { QStringList list; for (const auto &contact : contacts) { if (!contact.name.isEmpty()) { list << QString("%1 <%2>").arg(contact.name).arg(contact.emailAddress); } else { list << contact.emailAddress; } } return list.join(", "); } void MailListModel::fetchMail(Sink::ApplicationDomain::Mail::Ptr mail) { if (mail && !mail->getFullPayloadAvailable() && !mFetchedMails.contains(mail->identifier())) { qDebug() << "Fetching mail: " << mail->identifier() << mail->getSubject(); mFetchedMails.insert(mail->identifier()); Sink::Store::synchronize(Sink::SyncScope{*mail}).exec(); } } QVariant MailListModel::data(const QModelIndex &idx, int role) const { auto srcIdx = mapToSource(idx); auto mail = srcIdx.data(Sink::Store::DomainObjectRole).value(); switch (role) { case Subject: if (mail->isAggregate()) { return mail->getProperty(QByteArray{Sink::ApplicationDomain::Mail::Subject::name} + QByteArray{"Selected"}); } else { return mail->getSubject(); } case Sender: return mail->getSender().emailAddress; case SenderName: return mail->getSender().name; case To: return join(mail->getTo()); case Cc: return join(mail->getCc()); case Bcc: return join(mail->getBcc()); case Date: return mail->getDate(); case Unread: if (mail->isAggregate()) { return mail->getCollectedProperty().contains(true); } else { return mail->getUnread(); } case Important: if (mail->isAggregate()) { return mail->getCollectedProperty().contains(true); } else { return mail->getImportant(); } case Draft: return mail->getDraft(); case Sent: return mail->getSent(); case Trash: return mail->getTrash(); case Id: return mail->identifier(); case DomainObject: return QVariant::fromValue(mail); case MimeMessage: if (mFetchMails) { const_cast(this)->fetchMail(mail); } return mail->getMimeMessage(); case ThreadSize: return mail->count(); case Mail: return QVariant::fromValue(mail); case Incomplete: return !mail->getFullPayloadAvailable(); case Status: const auto status = srcIdx.data(Sink::Store::StatusRole).toInt(); if (status == Sink::ApplicationDomain::SyncStatus::SyncInProgress) { return InProgressStatus; } if (status == Sink::ApplicationDomain::SyncStatus::SyncError) { return ErrorStatus; } return NoStatus; } return QSortFilterProxyModel::data(idx, role); } bool MailListModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto leftDate = left.data(Sink::Store::DomainObjectRole).value()->getDate(); const auto rightDate = right.data(Sink::Store::DomainObjectRole).value()->getDate(); if (leftDate == rightDate) { return left.data(Sink::Store::DomainObjectRole).value()->identifier() < right.data(Sink::Store::DomainObjectRole).value()->identifier(); } return leftDate < rightDate; } bool MailListModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { auto idx = sourceModel()->index(sourceRow, 0, sourceParent); auto regExp = filterRegExp(); if (regExp.isEmpty()) { return true; } auto mail = idx.data(Sink::Store::DomainObjectRole).value(); return mail->getSubject().contains(regExp) || mail->getSender().name.contains(regExp); } void MailListModel::runQuery(const Sink::Query &query) { if (query.getBaseFilters().isEmpty() && query.ids().isEmpty()) { mQuery = {}; m_model.clear(); setSourceModel(nullptr); } else { mQuery = query; m_model = Sink::Store::loadModel(query); setSourceModel(m_model.data()); } } void MailListModel::setParentFolder(const QVariant &parentFolder) { using namespace Sink::ApplicationDomain; auto folder = parentFolder.value(); if (!folder) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == folder->identifier()) { return; } mCurrentQueryItem = folder->identifier(); const auto specialPurpose = folder->getSpecialPurpose(); mIsThreaded = !(specialPurpose.contains(SpecialPurpose::Mail::drafts) || specialPurpose.contains(SpecialPurpose::Mail::sent)); Sink::Query query = [&] { if (mIsThreaded) { return Sink::StandardQueries::threadLeaders(*folder); } else { Sink::Query query; query.setId("threadleaders-unthreaded"); if (!folder->resourceInstanceIdentifier().isEmpty()) { query.resourceFilter(folder->resourceInstanceIdentifier()); } query.filter(*folder); query.sort(); return query; } }(); if (!folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash)) { //Filter trash if this is not a trash folder query.filter(false); } query.setFlags(Sink::Query::LiveQuery); query.limit(100); requestHeaders(query); mFetchMails = false; qDebug() << "Running folder query: " << folder->resourceInstanceIdentifier() << folder->identifier(); //Latest mail on top sort(0, Qt::DescendingOrder); runQuery(query); } QVariant MailListModel::parentFolder() const { return QVariant(); } void MailListModel::setFilter(const QVariantMap &filter) { using namespace Sink::ApplicationDomain; + if (filter.contains("account")) { + mAccountId = filter.value("account").toByteArray(); + } + if (filter.contains("important") && filter.value("important").toBool()) { setShowImportant(true); return; } - if (filter.contains("folder")) { + if (filter.contains("drafts") && filter.value("drafts").toBool()) { + setShowDrafts(true); + return; + } + if (filter.contains("inbox") && filter.value("inbox").toBool()) { + qWarning() << "inbox"; + setShowInbox(true); + return; + } + if (filter.contains("folder") && filter.value("folder").value()) { setParentFolder(filter.value("folder")); return; } + //Clear + mCurrentQueryItem.clear(); + setSourceModel(nullptr); + return; } QVariantMap MailListModel::filter() const { return {}; } void MailListModel::setMail(const QVariant &variant) { using namespace Sink::ApplicationDomain; auto mail = variant.value(); if (!mail) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == mail->identifier()) { return; } mCurrentQueryItem = mail->identifier(); Sink::Query query = Sink::StandardQueries::completeThread(*mail); query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus); requestFullMail(query); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query: " << mail->resourceInstanceIdentifier() << mail->identifier(); //Latest mail at the bottom sort(0, Qt::AscendingOrder); runQuery(query); } QVariant MailListModel::mail() const { return QVariant(); } void MailListModel::setSingleMail(const QVariant &variant) { using namespace Sink::ApplicationDomain; auto mail = variant.value(); if (!mail) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == mail->identifier()) { return; } mCurrentQueryItem = mail->identifier(); Sink::Query query{*mail}; query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus); requestFullMail(query); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query: " << mail->resourceInstanceIdentifier() << mail->identifier(); //Latest mail at the bottom sort(0, Qt::AscendingOrder); runQuery(query); } QVariant MailListModel::singleMail() const { return {}; } void MailListModel::setShowDrafts(bool) { using namespace Sink::ApplicationDomain; Sink::Query query; query.setFlags(Sink::Query::LiveQuery); query.filter(true); query.filter(false); requestFullMail(query); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query for drafts: "; //Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } bool MailListModel::showDrafts() const { return false; } void MailListModel::setShowInbox(bool) { using namespace Sink::ApplicationDomain; Sink::Query folderQuery{}; folderQuery.containsFilter(Sink::ApplicationDomain::SpecialPurpose::Mail::inbox); folderQuery.request(); folderQuery.request(); Sink::Query query; query.setFlags(Sink::Query::LiveQuery); query.filter(folderQuery); query.sort(); requestHeaders(query); mFetchMails = false; mFetchedMails.clear(); //Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } bool MailListModel::showInbox() const { return false; } void MailListModel::setShowImportant(bool show) { if (!show) { return; } using namespace Sink::ApplicationDomain; mCurrentQueryItem.clear(); Sink::Query query; query.setFlags(Sink::Query::LiveQuery); + query.resourceFilter(mAccountId); query.filter(true); query.sort(); requestHeaders(query); mFetchMails = false; mFetchedMails.clear(); qDebug() << "Running mail query for drafts: "; //Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } bool MailListModel::showImportant() const { return false; } void MailListModel::setEntityId(const QString &id) { qDebug() << "Running mail query for mail with ID:" << id; if (id.isEmpty()) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == id) { return; } mCurrentQueryItem = id.toLatin1(); using namespace Sink::ApplicationDomain; Sink::Query query; query.setFlags(Sink::Query::LiveQuery); query.filter(id.toUtf8()); requestHeaders(query); mFetchMails = true; mFetchedMails.clear(); // Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } QString MailListModel::entityId() const { return {}; } diff --git a/framework/src/domain/maillistmodel.h b/framework/src/domain/maillistmodel.h index ceb638f3..b779b083 100644 --- a/framework/src/domain/maillistmodel.h +++ b/framework/src/domain/maillistmodel.h @@ -1,123 +1,124 @@ /* 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. */ #pragma once #include "kube_export.h" #include #include #include #include class KUBE_EXPORT MailListModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY (QVariant parentFolder READ parentFolder WRITE setParentFolder) Q_PROPERTY (QVariant mail READ mail WRITE setMail) Q_PROPERTY (QVariant singleMail READ singleMail WRITE setSingleMail) Q_PROPERTY (bool showDrafts READ showDrafts WRITE setShowDrafts) Q_PROPERTY (bool showInbox READ showInbox WRITE setShowInbox) Q_PROPERTY (bool showImportant READ showImportant WRITE setShowImportant) Q_PROPERTY (QString entityId READ entityId WRITE setEntityId) Q_PROPERTY (QVariantMap filter READ filter WRITE setFilter) Q_PROPERTY (QString filterString READ filterString WRITE setFilterString) Q_PROPERTY (bool threaded MEMBER mIsThreaded) public: enum Status { NoStatus, InProgressStatus, ErrorStatus }; Q_ENUMS(Status) MailListModel(QObject *parent = Q_NULLPTR); ~MailListModel(); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; enum Roles { Subject = Qt::UserRole + 1, Sender, SenderName, To, Cc, Bcc, Date, Unread, Important, Draft, Sent, Trash, Id, MimeMessage, DomainObject, ThreadSize, Mail, Incomplete, Status }; QHash roleNames() const Q_DECL_OVERRIDE; void runQuery(const Sink::Query &query); void setParentFolder(const QVariant &parentFolder); QVariant parentFolder() const; void setMail(const QVariant &mail); QVariant mail() const; void setSingleMail(const QVariant &mail); QVariant singleMail() const; void setFilterString(const QString &mail); QString filterString() const; void setFilter(const QVariantMap &); QVariantMap filter() const; void setShowDrafts(bool); bool showDrafts() const; void setShowInbox(bool); bool showInbox() const; void setShowImportant(bool); bool showImportant() const; void setEntityId(const QString &id); QString entityId() const; private: void fetchMail(Sink::ApplicationDomain::Mail::Ptr mail); QSharedPointer m_model; bool mFetchMails = false; bool mIsThreaded = true; QSet mFetchedMails; QByteArray mCurrentQueryItem; + QByteArray mAccountId; Sink::Query mQuery; }; diff --git a/views/conversation/qml/View.qml b/views/conversation/qml/View.qml index 8a4d7351..1335835a 100644 --- a/views/conversation/qml/View.qml +++ b/views/conversation/qml/View.qml @@ -1,351 +1,360 @@ /* * 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 var currentFolder: null property bool important: false //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}) } } onCurrentFolderChanged: { if (!!currentFolder) { root.important = false } } Kube.Listener { filter: Kube.Messages.search onMessageReceived: root.triggerSearch() } helpViewComponent: Kube.HelpPopup { ListModel { ListElement { description: qsTr("Jump to top of threadlist:"); shortcut: "t" } 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: ['t'] onActivated: Kube.Fabric.postMessage(Kube.Messages.selectTopConversation, {}) } 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: parent.left leftMargin: Kube.Units.largeSpacing right: parent.right rightMargin: Kube.Units.largeSpacing } property var currentListView: null function clearSelection() { if (accountFolderview.currentListView) { accountFolderview.currentListView.clearSelection() } } function selectRootIndex() { if (accountFolderview.currentListView) { accountFolderview.currentListView.selectRootIndex() } } delegate: ColumnLayout { id: delegateRoot - //Necessary to re-select on account change function currentChanged() { - if (isCurrent) { - listView.indexSelected(currentIndex) + if (delegateRoot.parent.isCurrent) { + //Reset important on account switch + root.important = false + //Necessary to re-select folder on account change (so the maillist is updated) + listView.indexSelected(listView.currentIndex) + //To ensure we always have something selected in the UI as well + if (!!listView.currentIndex) { + listView.selectRootIndex() + } } } property Component buttonDelegate: Row { Kube.IconButton { height: Kube.Units.gridUnit padding: 0 iconName: Kube.Icons.markImportant_inverted checked: root.important checkable: true onToggled: { if (checked) { root.important = true accountFolderview.clearSelection() } else { root.important = true accountFolderview.selectRootIndex() } } } //TODO: edit mode // Kube.IconButton { // height: Kube.Units.gridUnit // padding: 0 // iconName: Kube.Icons.overflowMenu_inverted // onClicked: listView.editMode = !listView.editMode; // checkable: true // checked: listView.editMode // } } Kube.FolderListView { id: listView objectName: "folderListView" accountId: delegateRoot.parent.accountId Layout.fillWidth: true Layout.fillHeight: true function indexSelected(currentIndex) { - if (currentIndex.valid) { + if (!!currentIndex && currentIndex.valid) { root.currentFolder = model.data(currentIndex, Kube.FolderListModel.DomainObject) Kube.Fabric.postMessage(Kube.Messages.folderSelection, {"folder": root.currentFolder, "trash": model.data(currentIndex, Kube.FolderListModel.Trash)}) } else { root.currentFolder = null + Kube.Fabric.postMessage(Kube.Messages.folderSelection, {"folder": null, + "trash": false}) } } onCurrentIndexChanged: { accountFolderview.currentListView = listView - if (isCurrent) { + if (delegateRoot.parent.isCurrent) { indexSelected(currentIndex) } } onDropped: { var folder = model.data(index, Kube.FolderListModel.DomainObject) Kube.Fabric.postMessage(Kube.Messages.moveToFolder, {"mail": drop.source.mail, "folder": folder}) drop.accept(Qt.MoveAction) } } } } 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 showImportant: root.important + currentAccount: Kube.Context.currentAccountId 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 } } } } }