diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp index 4bfc34ab..09d4c154 100644 --- a/framework/src/domain/composercontroller.cpp +++ b/framework/src/domain/composercontroller.cpp @@ -1,544 +1,568 @@ /* 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 "composercontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include "identitiesmodel.h" #include "recepientautocompletionmodel.h" #include "mime/mailtemplates.h" #include "mime/mailcrypto.h" #include "async.h" std::vector &operator+=(std::vector &list, const std::vector &add) { list.insert(std::end(list), std::begin(add), std::end(add)); return list; } class IdentitySelector : public Selector { public: IdentitySelector(ComposerController &controller) : Selector(new IdentitiesModel), mController(controller) { } void setCurrent(const QModelIndex &index) Q_DECL_OVERRIDE { if (index.isValid()) { auto currentAccountId = index.data(IdentitiesModel::AccountId).toByteArray(); KMime::Types::Mailbox mb; mb.setName(index.data(IdentitiesModel::Username).toString()); mb.setAddress(index.data(IdentitiesModel::Address).toString().toUtf8()); SinkLog() << "Setting current identity: " << mb.prettyAddress() << "Account: " << currentAccountId; mController.setIdentity(mb); mController.setAccountId(currentAccountId); } else { SinkWarning() << "No valid identity for index: " << index; mController.clearIdentity(); mController.clearAccountId(); } } QVector getAllAddresses() { QVector list; for (int i = 0; i < model()->rowCount(); i++) { list << model()->data(model()->index(i, 0), IdentitiesModel::Address).toString().toUtf8(); } return list; } private: ComposerController &mController; }; class RecipientCompleter : public Completer { public: RecipientCompleter() : Completer(new RecipientAutocompletionModel) { } void setSearchString(const QString &s) { static_cast(model())->setFilter(s); Completer::setSearchString(s); } }; class AddresseeController : public Kube::ListPropertyController { + Q_OBJECT + Q_PROPERTY(bool foundAllKeys READ foundAllKeys NOTIFY foundAllKeysChanged) public: + bool mFoundAllKeys = true; QSet mMissingKeys; AddresseeController() : Kube::ListPropertyController{{"name", "keyFound", "key"}} { QObject::connect(this, &Kube::ListPropertyController::added, this, [this] (const QByteArray &id, const QVariantMap &map) { findKey(id, map.value("name").toString()); }); + QObject::connect(this, &Kube::ListPropertyController::removed, this, [this] (const QByteArray &id) { + mMissingKeys.remove(id); + setFoundAllKeys(mMissingKeys.isEmpty()); + }); + + + } + + bool foundAllKeys() + { + return mFoundAllKeys; + } + + void setFoundAllKeys(bool found) + { + mFoundAllKeys = found; + emit foundAllKeysChanged(); } void findKey(const QByteArray &id, const QString &addressee) { mMissingKeys << id; + setFoundAllKeys(mMissingKeys.isEmpty()); KMime::Types::Mailbox mb; mb.fromUnicodeString(addressee); SinkLog() << "Searching key for: " << mb.address(); asyncRun>(this, [mb] { return MailCrypto::findKeys(QStringList{} << mb.address(), false, false, MailCrypto::OPENPGP); }, [this, addressee, id](const std::vector &keys) { if (!keys.empty()) { if (keys.size() > 1 ) { SinkWarning() << "Found more than one key, encrypting to all of them."; } SinkLog() << "Found key: " << keys.front().primaryFingerprint(); setValue(id, "keyFound", true); setValue(id, "key", QVariant::fromValue(keys)); mMissingKeys.remove(id); - setProperty("foundAllKeys", mMissingKeys.isEmpty()); + setFoundAllKeys(mMissingKeys.isEmpty()); } else { SinkWarning() << "Failed to find key for recipient."; } }); } void set(const QStringList &list) { for (const auto &email: list) { add({{"name", email}}); } } +signals: + void foundAllKeysChanged(); }; class AttachmentController : public Kube::ListPropertyController { public: AttachmentController() : Kube::ListPropertyController{{"name", "filename", "content", "mimetype", "description", "iconname", "url", "inline"}} { QObject::connect(this, &Kube::ListPropertyController::added, this, [this] (const QByteArray &id, const QVariantMap &map) { auto url = map.value("url").toUrl(); setAttachmentProperties(id, url); }); } void setAttachmentProperties(const QByteArray &id, const QUrl &url) { QMimeDatabase db; auto mimeType = db.mimeTypeForUrl(url); if (mimeType.name() == QLatin1String("inode/directory")) { qWarning() << "Can't deal with directories yet."; } else { if (!url.isLocalFile()) { qWarning() << "Cannot attach remote file: " << url; return; } QFileInfo fileInfo(url.toLocalFile()); if (!fileInfo.exists()) { qWarning() << "The file doesn't exist: " << url; } QFile file{fileInfo.filePath()}; file.open(QIODevice::ReadOnly); const auto data = file.readAll(); QVariantMap map; map.insert("filename", fileInfo.fileName()); map.insert("mimetype", mimeType.name().toLatin1()); map.insert("filename", fileInfo.fileName().toLatin1()); map.insert("inline", false); map.insert("iconname", mimeType.iconName()); map.insert("url", url); map.insert("content", data); setValues(id, map); } } }; ComposerController::ComposerController() : Kube::Controller(), controller_to{new AddresseeController}, controller_cc{new AddresseeController}, controller_bcc{new AddresseeController}, controller_attachments{new AttachmentController}, action_send{new Kube::ControllerAction{this, &ComposerController::send}}, action_saveAsDraft{new Kube::ControllerAction{this, &ComposerController::saveAsDraft}}, mRecipientCompleter{new RecipientCompleter}, mIdentitySelector{new IdentitySelector{*this}} { QObject::connect(this, &ComposerController::identityChanged, &ComposerController::findPersonalKey); } void ComposerController::findPersonalKey() { auto identity = getIdentity(); SinkLog() << "Looking for personal key for: " << identity.address(); asyncRun>(this, [=] { return MailCrypto::findKeys(QStringList{} << identity.address(), true); }, [this](const std::vector &keys) { if (keys.empty()) { SinkWarning() << "Failed to find a personal key."; } else if (keys.size() > 1) { SinkWarning() << "Found multiple keys, using all of them."; } setPersonalKeys(QVariant::fromValue(keys)); setFoundPersonalKeys(!keys.empty()); }); } void ComposerController::clear() { Controller::clear(); //Reapply account and identity from selection mIdentitySelector->reapplyCurrentIndex(); //FIXME implement in Controller::clear instead toController()->clear(); ccController()->clear(); bccController()->clear(); } Completer *ComposerController::recipientCompleter() const { return mRecipientCompleter.data(); } Selector *ComposerController::identitySelector() const { return mIdentitySelector.data(); } static void applyAddresses(const KMime::Types::Mailbox::List &list, std::function callback) { for (const auto &to : list) { callback(to.address(), to.name().toUtf8()); } } static void applyAddresses(const QStringList &list, std::function callback) { KMime::Types::Mailbox::List mailboxes; for (const auto &s : list) { KMime::Types::Mailbox mb; mb.fromUnicodeString(s); mailboxes << mb; } applyAddresses(mailboxes, callback); } static QStringList getStringListFromAddresses(const KMime::Types::Mailbox::List &s) { QStringList list; applyAddresses(s, [&](const QByteArray &addrSpec, const QByteArray &displayName) { if (displayName.isEmpty()) { list << QString{addrSpec}; } else { list << QString("%1 <%2>").arg(QString{displayName}).arg(QString{addrSpec}); } }); return list; } void ComposerController::addAttachmentPart(KMime::Content *partToAttach) { QVariantMap map; // May need special care for the multipart/digest MIME type map.insert("content", partToAttach->decodedContent()); map.insert("mimetype", partToAttach->contentType()->mimeType()); QMimeDatabase db; auto mimeType = db.mimeTypeForName(partToAttach->contentType()->mimeType()); map.insert("iconname", mimeType.iconName()); if (partToAttach->contentDescription(false)) { map.insert("description", partToAttach->contentDescription()->asUnicodeString()); } QString name; QString filename; if (partToAttach->contentType(false)) { if (partToAttach->contentType()->hasParameter(QStringLiteral("name"))) { name = partToAttach->contentType()->parameter(QStringLiteral("name")); } } if (partToAttach->contentDisposition(false)) { filename = partToAttach->contentDisposition()->filename(); map.insert("inline", partToAttach->contentDisposition()->disposition() == KMime::Headers::CDinline); } if (name.isEmpty() && !filename.isEmpty()) { name = filename; } if (filename.isEmpty() && !name.isEmpty()) { filename = name; } if (!filename.isEmpty()) { map.insert("filename", filename); } if (!name.isEmpty()) { map.insert("name", name); } attachmentsController()->add(map); } void ComposerController::setMessage(const KMime::Message::Ptr &msg) { static_cast(toController())->set(getStringListFromAddresses(msg->to(true)->mailboxes())); static_cast(ccController())->set(getStringListFromAddresses(msg->cc(true)->mailboxes())); static_cast(bccController())->set(getStringListFromAddresses(msg->bcc(true)->mailboxes())); setSubject(msg->subject(true)->asUnicodeString()); bool isHtml = false; const auto body = MailTemplates::body(msg, isHtml); setHtmlBody(isHtml); setBody(body); //TODO use ObjecTreeParser to get encrypted attachments as well foreach (const auto &att, msg->attachments()) { addAttachmentPart(att); } setExistingMessage(msg); } void ComposerController::loadDraft(const QVariant &message) { loadMessage(message, [this] (const KMime::Message::Ptr &mail) { mRemoveDraft = true; setMessage(mail); }); } void ComposerController::loadReply(const QVariant &message) { loadMessage(message, [this] (const KMime::Message::Ptr &mail) { //Find all personal email addresses to exclude from reply KMime::Types::AddrSpecList me; auto list = static_cast(mIdentitySelector.data())->getAllAddresses(); for (const auto &a : list) { KMime::Types::Mailbox mb; mb.setAddress(a); me << mb.addrSpec(); } MailTemplates::reply(mail, [this] (const KMime::Message::Ptr &reply) { //We assume reply setMessage(reply); }, me); }); } void ComposerController::loadForward(const QVariant &message) { loadMessage(message, [this] (const KMime::Message::Ptr &mail) { MailTemplates::forward(mail, [this] (const KMime::Message::Ptr &fwdMessage) { setMessage(fwdMessage); }); }); } void ComposerController::loadMessage(const QVariant &message, std::function callback) { using namespace Sink; using namespace Sink::ApplicationDomain; auto msg = message.value(); Q_ASSERT(msg); Query query(*msg); query.request(); Store::fetchOne(query).then([this, callback](const Mail &mail) { setExistingMail(mail); const auto mailData = KMime::CRLFtoLF(mail.getMimeMessage()); if (!mailData.isEmpty()) { KMime::Message::Ptr mail(new KMime::Message); mail->setContent(mailData); mail->parse(); callback(mail); } else { qWarning() << "Retrieved empty message"; } }).exec(); } void ComposerController::recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName) { if (auto model = static_cast(recipientCompleter()->model())) { model->addEntry(addrSpec, displayName); } } std::vector ComposerController::getRecipientKeys() { std::vector keys; { const auto list = toController()->getList>("key"); for (const auto &l: list) { keys.insert(std::end(keys), std::begin(l), std::end(l)); } } { const auto list = ccController()->getList>("key"); for (const auto &l: list) { keys.insert(std::end(keys), std::begin(l), std::end(l)); } } { const auto list = bccController()->getList>("key"); for (const auto &l: list) { keys.insert(std::end(keys), std::begin(l), std::end(l)); } } return keys; } KMime::Message::Ptr ComposerController::assembleMessage() { auto toAddresses = toController()->getList("name"); auto ccAddresses = ccController()->getList("name"); auto bccAddresses = bccController()->getList("name"); applyAddresses(toAddresses + ccAddresses + bccAddresses, [&](const QByteArray &addrSpec, const QByteArray &displayName) { recordForAutocompletion(addrSpec, displayName); }); QList attachments; attachmentsController()->traverse([&](const QVariantMap &value) { attachments << Attachment{ value["name"].toString(), value["filename"].toString(), value["mimetype"].toByteArray(), value["inline"].toBool(), value["content"].toByteArray() }; }); std::vector signingKeys; if (getSign()) { signingKeys = getPersonalKeys().value>(); } std::vector encryptionKeys; if (getEncrypt()) { //Encrypt to self so we can read the sent message encryptionKeys += getPersonalKeys().value>(); encryptionKeys += getRecipientKeys(); } return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys); } void ComposerController::send() { auto message = assembleMessage(); if (!message) { SinkWarning() << "Failed to assemble the message."; return; } auto accountId = getAccountId(); //SinkLog() << "Sending a mail: " << *this; using namespace Sink; using namespace Sink::ApplicationDomain; Q_ASSERT(!accountId.isEmpty()); Query query; query.containsFilter(ResourceCapabilities::Mail::transport); query.filter(accountId); auto job = Store::fetchAll(query) .then([=](const QList &resources) { if (!resources.isEmpty()) { auto resourceId = resources[0]->identifier(); SinkLog() << "Sending message via resource: " << resourceId; Mail mail(resourceId); mail.setMimeMessage(message->encodedContent(true)); return Store::create(mail) .then([=] { //Trigger a sync, but don't wait for it. Store::synchronize(Sink::SyncScope{}.resourceFilter(resourceId)).exec(); if (mRemoveDraft) { SinkLog() << "Removing draft message."; //Remove draft Store::remove(getExistingMail()).exec(); } }); } SinkWarning() << "Failed to find a mailtransport resource"; return KAsync::error(0, "Failed to find a MailTransport resource."); }) .then([&] (const KAsync::Error &) { SinkLog() << "Message was sent: "; emit done(); }); run(job); } void ComposerController::saveAsDraft() { SinkLog() << "Save as draft"; const auto accountId = getAccountId(); auto existingMail = getExistingMail(); auto message = assembleMessage(); if (!message) { SinkWarning() << "Failed to assemble the message."; return; } using namespace Sink; using namespace Sink::ApplicationDomain; auto job = [&] { if (existingMail.identifier().isEmpty()) { SinkLog() << "Creating a new draft" << existingMail.identifier(); Query query; query.containsFilter(ResourceCapabilities::Mail::drafts); query.filter(accountId); return Store::fetchOne(query) .then([=](const SinkResource &resource) { Mail mail(resource.identifier()); mail.setDraft(true); mail.setMimeMessage(message->encodedContent(true)); return Store::create(mail); }) .onError([] (const KAsync::Error &error) { SinkWarning() << "Error while creating draft: " << error.errorMessage; }); } else { SinkLog() << "Modifying an existing mail" << existingMail.identifier(); existingMail.setDraft(true); existingMail.setMimeMessage(message->encodedContent(true)); return Store::modify(existingMail); } }(); job = job.then([&] (const KAsync::Error &) { emit done(); }); run(job); } +#include "composercontroller.moc" diff --git a/framework/src/domain/controller.cpp b/framework/src/domain/controller.cpp index 226615ad..09fe0b9d 100644 --- a/framework/src/domain/controller.cpp +++ b/framework/src/domain/controller.cpp @@ -1,177 +1,178 @@ /* 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 "controller.h" #include #include #include #include #include #include using namespace Kube; ControllerState::ControllerState() : QObject() { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } ControllerAction::ControllerAction() : ControllerState() { } void ControllerAction::execute() { emit triggered(); } void Controller::clear() { auto meta = metaObject(); //We want to get the offset for this class, but clear the properties of all subclasses (thus staticMetaObject for the offset) for (auto i = staticMetaObject.propertyOffset(); i < meta->propertyCount(); i++) { auto property = meta->property(i); setProperty(property.name(), QVariant()); } for (const auto &p : dynamicPropertyNames()) { setProperty(p, QVariant()); } } void Controller::run(const KAsync::Job &job) { auto jobToExec = job; jobToExec.onError([] (const KAsync::Error &error) { SinkWarningCtx(Sink::Log::Context{"controller"}) << "Error while executing job: " << error.errorMessage; }); //TODO handle error //TODO attach a log context to the execution that we can gather from the job? jobToExec.exec(); } static void traverse(const QStandardItemModel *model, const std::function &f) { auto root = model->invisibleRootItem(); for (int row = 0; row < root->rowCount(); row++) { if (!f(root->child(row, 0))) { return; } } } ListPropertyController::ListPropertyController(const QStringList &roles) : QObject(), mModel(new QStandardItemModel) { //Generate a set of roles for the names. We're not using any enum, so the actual role value doesn't matter. int role = Qt::UserRole + 1; mRoles.insert("id", role); role++; for (const auto &r : roles) { mRoles.insert(r, role); role++; } QHash roleNames; for (const auto r : mRoles.keys()) { roleNames.insert(mRoles[r], r.toLatin1()); } mModel->setItemRoleNames(roleNames); } void ListPropertyController::add(const QVariantMap &value) { auto item = new QStandardItem; auto id = QUuid::createUuid().toByteArray(); item->setData(id, mRoles["id"]); for (const auto &k : value.keys()) { item->setData(value.value(k), mRoles[k]); } mModel->appendRow(QList() << item); if (mModel->rowCount() <= 1) { emit emptyChanged(); } emit added(id, value); } void ListPropertyController::remove(const QByteArray &id) { auto root = mModel->invisibleRootItem(); const auto idRole = mRoles["id"]; for (int row = 0; row < root->rowCount(); row++) { if (root->child(row, 0)->data(idRole).toByteArray() == id) { root->removeRow(row); break; } } + emit removed(id); if (mModel->rowCount() <= 0) { emit emptyChanged(); } } bool ListPropertyController::empty() const { return mModel->rowCount() == 0; } void ListPropertyController::clear() { mModel->clear(); } QAbstractItemModel *ListPropertyController::model() { QQmlEngine::setObjectOwnership(mModel.data(), QQmlEngine::CppOwnership); return mModel.data(); } void ListPropertyController::setValue(const QByteArray &id, const QString &key, const QVariant &value) { setValues(id, {{key, value}}); } void ListPropertyController::setValues(const QByteArray &id, const QVariantMap &values) { const auto idRole = mRoles["id"]; ::traverse(mModel.data(), [&] (QStandardItem *item) { if (item->data(idRole).toByteArray() == id) { for (const auto &key : values.keys()) { item->setData(values.value(key), mRoles[key]); } return false; } return true; }); } void ListPropertyController::traverse(const std::function &f) { ::traverse(mModel.data(), [&] (QStandardItem *item) { QVariantMap map; for (const auto &key : mRoles.keys()) { map.insert(key, item->data(mRoles[key])); } f(map); return true; }); } diff --git a/framework/src/domain/controller.h b/framework/src/domain/controller.h index 5f610012..9166a63a 100644 --- a/framework/src/domain/controller.h +++ b/framework/src/domain/controller.h @@ -1,150 +1,151 @@ /* 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 #include #include #define KUBE_CONTROLLER_PROPERTY(TYPE, NAME, LOWERCASENAME) \ public: Q_PROPERTY(TYPE LOWERCASENAME MEMBER m##NAME NOTIFY LOWERCASENAME##Changed) \ Q_SIGNALS: void LOWERCASENAME##Changed(); \ private: TYPE m##NAME; \ public: \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ void set##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \ void clear##NAME() { setProperty(NAME::name, QVariant{}); } \ TYPE get##NAME() const { return m##NAME; } \ #define KUBE_CONTROLLER_ACTION(NAME) \ Q_PROPERTY (Kube::ControllerAction* NAME##Action READ NAME##Action CONSTANT) \ private: QScopedPointer action_##NAME; \ public: Kube::ControllerAction* NAME##Action() const { Q_ASSERT(action_##NAME); return action_##NAME.data(); } \ private slots: void NAME(); \ #define KUBE_CONTROLLER_LISTCONTROLLER(NAME) \ Q_PROPERTY (Kube::ListPropertyController* NAME READ NAME##Controller CONSTANT) \ private: QScopedPointer controller_##NAME; \ public: Kube::ListPropertyController* NAME##Controller() const { Q_ASSERT(controller_##NAME); return controller_##NAME.data(); } \ class QAbstractItemModel; class QStandardItemModel; namespace Kube { class ControllerState : public QObject { Q_OBJECT Q_PROPERTY(bool enabled MEMBER mEnabled NOTIFY enabledChanged) public: ControllerState(); ~ControllerState() = default; void setEnabled(bool enabled) { setProperty("enabled", enabled); } signals: void enabledChanged(); private: bool mEnabled = false; }; class ControllerAction : public ControllerState { Q_OBJECT public: ControllerAction(); template ControllerAction(const typename QtPrivate::FunctionPointer::Object *obj, Func slot) : ControllerAction() { QObject::connect(this, &ControllerAction::triggered, obj, slot); } ~ControllerAction() = default; Q_INVOKABLE void execute(); signals: void triggered(); }; class Controller : public QObject { Q_OBJECT public: Controller() = default; virtual ~Controller() = default; public slots: virtual void clear(); signals: void done(); void error(); protected: void run(const KAsync::Job &job); }; class ListPropertyController : public QObject { Q_OBJECT Q_PROPERTY(QAbstractItemModel* model READ model CONSTANT) Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged) public: ListPropertyController(const QStringList &roles); Q_INVOKABLE virtual void add(const QVariantMap &value); Q_INVOKABLE virtual void remove(const QByteArray &id); Q_INVOKABLE void clear(); QAbstractItemModel *model(); void setValue(const QByteArray &id, const QString &key, const QVariant &); void setValues(const QByteArray &id, const QVariantMap &values); void traverse(const std::function &f); template QList getList(const QString &property) { QList list; traverse([&] (const QVariantMap &map) { list << map[property].value(); }); return list; } bool empty() const; Q_SIGNALS: void added(QByteArray, QVariantMap); + void removed(QByteArray); void emptyChanged(); protected: QScopedPointer mModel; private: QHash mRoles; }; } diff --git a/views/composer/qml/View.qml b/views/composer/qml/View.qml index 1ea67513..d444ea47 100644 --- a/views/composer/qml/View.qml +++ b/views/composer/qml/View.qml @@ -1,543 +1,543 @@ /* * 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.7 import QtQuick.Controls 1.3 import QtQuick.Controls 2.0 as Controls2 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.0 as Dialogs import org.kube.framework 1.0 as Kube Kube.View { id: root property bool newMessage: false property int loadType: Kube.ComposerController.Draft property variant message: {} property variant recipients: [] resources: [ Kube.ComposerController { id: composerController htmlBody: html.checked sign: signCheckbox.checked encrypt: encryptCheckbox.checked onDone: Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) - property bool foundAllKeys: to.foundAllKeys && cc.foundAllKeys && bcc.foundAllKeys + property bool foundAllKeys: composerController.to.foundAllKeys && composerController.cc.foundAllKeys && composerController.bcc.foundAllKeys sendAction.enabled: composerController.accountId && composerController.subject && (!composerController.encrypt || composerController.foundAllKeys) && (!composerController.sign && !composerController.encrypt || composerController.foundPersonalKeys) && !composerController.to.empty saveAsDraftAction.enabled: composerController.accountId } ] onSetup: { loadMessage(root.message, root.loadAsDraft) Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "mail", "specialPurpose": "drafts"}) //For autocompletion Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "contacts"}) } function loadMessage(message, loadAsDraft) { if (message) { switch(loadType) { case Kube.ComposerController.Draft: composerController.loadDraft(message) break; case Kube.ComposerController.Reply: composerController.loadReply(message) subject.forceActiveFocus() break; case Kube.ComposerController.Forward: composerController.loadForward(message) subject.forceActiveFocus() break; } } else if (newMessage) { composerController.clear() if (root.recipients) { for (var i = 0; i < root.recipients.length; ++i) { composerController.to.add({name: root.recipients[i]}) } } subject.forceActiveFocus() } } function closeFirstSplitIfNecessary() { //Move the view forward if (root.currentIndex == 0) { root.incrementCurrentIndex() } } //Drafts Rectangle { anchors { top: parent.top bottom: parent.bottom } width: Kube.Units.gridUnit * 10 Layout.minimumWidth: Kube.Units.gridUnit * 5 color: Kube.Colors.textColor ColumnLayout { anchors { fill: parent margins: Kube.Units.largeSpacing } spacing: Kube.Units.largeSpacing Kube.PositiveButton { objectName: "newMailButton" anchors { left: parent.left right: parent.right } focus: true text: qsTr("New Email") onClicked: { listView.currentIndex = -1 composerController.clear() subject.forceActiveFocus() } } Kube.Label{ text: qsTr("Drafts") color: Kube.Colors.highlightedTextColor } Kube.ListView { id: listView activeFocusOnTab: true anchors { left: parent.left right: parent.right } Layout.fillHeight: true clip: true currentIndex: -1 highlightFollowsCurrentItem: false //BEGIN keyboard nav onActiveFocusChanged: { if (activeFocus && currentIndex < 0) { currentIndex = 0 } } Keys.onDownPressed: { listView.incrementCurrentIndex() } Keys.onUpPressed: { listView.decrementCurrentIndex() } //END keyboard nav onCurrentItemChanged: { if (currentItem) { root.loadMessage(currentItem.currentData.domainObject, true) } } model: Kube.MailListModel { id: mailListModel showDrafts: true } delegate: Kube.ListDelegate { id: delegateRoot color: Kube.Colors.textColor border.width: 0 Item { id: content anchors { fill: parent margins: Kube.Units.smallSpacing } Kube.Label { width: content.width text: model.subject == "" ? "no subject" : model.subject color: Kube.Colors.highlightedTextColor maximumLineCount: 2 wrapMode: Text.WrapAnywhere elide: Text.ElideRight } Kube.Label { anchors { right: parent.right bottom: parent.bottom } text: Qt.formatDateTime(model.date, "dd MMM yyyy") font.italic: true color: Kube.Colors.disabledTextColor font.pointSize: Kube.Units.smallFontSize visible: !delegateRoot.hovered } } Row { id: buttons anchors { right: parent.right bottom: parent.bottom margins: Kube.Units.smallSpacing } visible: delegateRoot.hovered spacing: Kube.Units.smallSpacing opacity: 0.7 Kube.IconButton { id: deleteButton activeFocusOnTab: true iconName: Kube.Icons.moveToTrash visible: enabled enabled: !!model.mail onClicked: Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": model.mail}) } } } } } } //Content Rectangle { Layout.fillWidth: true Layout.minimumWidth: Kube.Units.gridUnit * 5 anchors { top: parent.top bottom: parent.bottom } color: Kube.Colors.backgroundColor ColumnLayout { anchors { fill: parent margins: Kube.Units.largeSpacing leftMargin: Kube.Units.largeSpacing + Kube.Units.gridUnit * 2 rightMargin: Kube.Units.largeSpacing + Kube.Units.gridUnit * 2 } spacing: Kube.Units.smallSpacing Kube.TextField { id: subject objectName: "subject" Layout.fillWidth: true activeFocusOnTab: true placeholderText: qsTr("Enter Subject...") text: composerController.subject onTextChanged: composerController.subject = text; onActiveFocusChanged: { if (activeFocus) { closeFirstSplitIfNecessary() } } } Flow { id: attachments Layout.fillWidth: true layoutDirection: Qt.RightToLeft spacing: Kube.Units.smallSpacing clip: true Repeater { model: composerController.attachments.model delegate: Kube.AttachmentDelegate { name: model.filename icon: model.iconname clip: true actionIcon: Kube.Icons.remove onExecute: composerController.attachments.remove(model.id) } } } RowLayout { spacing: Kube.Units.largeSpacing Kube.Switch { id: html text: checked ? qsTr("plain") : qsTr("html") focusPolicy: Qt.TabFocus focus: false checked: composerController.htmlBody } Row { visible: html.checked spacing: 1 Kube.IconButton { iconName: Kube.Icons.bold checkable: true checked: textEditor.bold onClicked: textEditor.bold = !textEditor.bold focusPolicy: Qt.TabFocus focus: false } Kube.IconButton { iconName: Kube.Icons.italic checkable: true checked: textEditor.italic onClicked: textEditor.italic = !textEditor.italic focusPolicy: Qt.TabFocus focus: false } Kube.IconButton { iconName: Kube.Icons.underline checkable: true checked: textEditor.underline onClicked: textEditor.underline = !textEditor.underline focusPolicy: Qt.TabFocus focus: false } } Item { height: 1 Layout.fillWidth: true } Kube.Button { text: qsTr("Attach file") onClicked: { fileDialogComponent.createObject(parent) } Component { id: fileDialogComponent Dialogs.FileDialog { id: fileDialog visible: true title: "Choose a file to attach" selectFolder: false onAccepted: { composerController.attachments.add({url: fileDialog.fileUrl}) } } } } } Kube.TextEditor { id: textEditor Layout.fillWidth: true Layout.fillHeight: true htmlEnabled: html.checked onActiveFocusChanged: closeFirstSplitIfNecessary() Keys.onEscapePressed: recipients.forceActiveFocus() initialText: composerController.body onTextChanged: composerController.body = text; } } } //Recepients FocusScope { id: recipients anchors { top: parent.top bottom: parent.bottom } width: Kube.Units.gridUnit * 15 activeFocusOnTab: true //background Rectangle { anchors.fill: parent color: Kube.Colors.backgroundColor Rectangle { height: parent.height width: 1 color: Kube.Colors.buttonColor } } //Content ColumnLayout { anchors { fill: parent margins: Kube.Units.largeSpacing } spacing: Kube.Units.largeSpacing ColumnLayout { Layout.maximumWidth: parent.width Layout.fillWidth: true Layout.fillHeight: true Kube.Label { text: qsTr("Sending Email to:") } AddresseeListEditor { Layout.preferredHeight: implicitHeight Layout.fillWidth: true focus: true activeFocusOnTab: true encrypt: composerController.encrypt controller: composerController.to completer: composerController.recipientCompleter } Kube.Label { text: qsTr("Sending Copy to (CC):") } AddresseeListEditor { id: cc Layout.preferredHeight: cc.implicitHeight Layout.fillWidth: true activeFocusOnTab: true encrypt: composerController.encrypt controller: composerController.cc completer: composerController.recipientCompleter } Kube.Label { text: qsTr("Sending Secret Copy to (Bcc):") } AddresseeListEditor { id: bcc Layout.preferredHeight: bcc.implicitHeight Layout.fillWidth: true activeFocusOnTab: true encrypt: composerController.encrypt controller: composerController.bcc completer: composerController.recipientCompleter } Item { width: parent.width Layout.fillHeight: true } } RowLayout { enabled: composerController.foundPersonalKeys Kube.CheckBox { id: encryptCheckbox checked: composerController.encrypt } Kube.Label { text: qsTr("encrypt") } } RowLayout { enabled: composerController.foundPersonalKeys Kube.CheckBox { id: signCheckbox checked: composerController.sign } Kube.Label { text: qsTr("sign") } } Kube.Label { visible: !composerController.foundPersonalKeys Layout.maximumWidth: parent.width text: qsTr("Encryption is not available because your personal key has not been found.") wrapMode: Text.Wrap } RowLayout { Layout.maximumWidth: parent.width width: parent.width height: Kube.Units.gridUnit Kube.Button { width: saveDraftButton.width text: qsTr("Discard") onClicked: Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) } Kube.Button { id: saveDraftButton text: qsTr("Save as Draft") enabled: composerController.saveAsDraftAction.enabled onClicked: { composerController.saveAsDraftAction.execute() } } } ColumnLayout { Layout.maximumWidth: parent.width Layout.fillWidth: true Kube.Label { id: fromLabel text: qsTr("You are sending this from:") } Kube.ComboBox { id: identityCombo width: parent.width - Kube.Units.largeSpacing * 2 model: composerController.identitySelector.model textRole: "address" Layout.fillWidth: true onCurrentIndexChanged: { composerController.identitySelector.currentIndex = currentIndex } } } Kube.PositiveButton { objectName: "sendButton" id: sendButton width: parent.width text: qsTr("Send") enabled: composerController.sendAction.enabled onClicked: { composerController.sendAction.execute() } } } }//FocusScope }