diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp index 4aafc486..c62271f1 100644 --- a/framework/src/domain/composercontroller.cpp +++ b/framework/src/domain/composercontroller.cpp @@ -1,599 +1,604 @@ /* 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 { Q_OBJECT Q_PROPERTY (QString currentAccountId WRITE setCurrentAccountId) 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(); } } void setCurrentAccountId(const QString &accountId) { for (int i = 0; i < model()->rowCount(); i++) { if (model()->index(i, 0).data(IdentitiesModel::AccountId).toString() == accountId) { setCurrentIndex(i); return; } } } 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", "fetching"}} { QObject::connect( this, &Kube::ListPropertyController::added, this, [this](const QByteArray &id, const QVariantMap &map) { findKey(id, map.value("name").toString(), false); }); 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, bool fetchRemote) { mMissingKeys << id; setFoundAllKeys(false); KMime::Types::Mailbox mb; mb.fromUnicodeString(addressee); SinkLog() << "Searching key for: " << mb.address(); setValue(id, "fetching", fetchRemote); asyncRun>(this, [=] { return Crypto::findKeys({mb.address()}, false, fetchRemote); }, [this, addressee, id](const std::vector &keys) { setValue(id, "fetching", false); if (!keys.empty()) { if (keys.size() > 1) { SinkWarning() << "Found more than one key, encrypting to all of them."; } SinkLog() << "Found key: " << keys.front(); setValue(id, "keyFound", true); setValue(id, "key", QVariant::fromValue(keys)); mMissingKeys.remove(id); setFoundAllKeys(mMissingKeys.isEmpty()); } else { SinkWarning() << "Failed to find key for recipient."; } }); } Q_INVOKABLE void fetchKeys(const QByteArray &id, const QString &addressee) { findKey(id, addressee, true); } 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 Crypto::findKeys({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); emit messageLoaded(body); } void ComposerController::loadDraft(const QVariant &message) { loadMessage(message, [this] (const KMime::Message::Ptr &mail) { - mRemoveDraft = true; - setMessage(mail); - }); + setEncrypt(KMime::isEncrypted(mail.data())); + setSign(KMime::isSigned(mail.data())); + 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(); - } + //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); - }); + setEncrypt(KMime::isEncrypted(mail.data())); + setSign(KMime::isSigned(mail.data())); + MailTemplates::reply(mail, [this] (const auto &msg) { + setMessage(msg); + }, 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); - }); - }); + setEncrypt(KMime::isEncrypted(mail.data())); + setSign(KMime::isSigned(mail.data())); + MailTemplates::forward(mail, [this] (const auto &msg) { + setMessage(msg); + }); + }); } 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() }; }); Crypto::Key attachedKey; std::vector signingKeys; if (getSign()) { signingKeys = getPersonalKeys().value>(); Q_ASSERT(!signingKeys.empty()); attachedKey = signingKeys[0]; } std::vector encryptionKeys; if (getEncrypt()) { //Encrypt to self so we can read the sent message auto personalKeys = getPersonalKeys().value>(); attachedKey = personalKeys[0]; encryptionKeys += personalKeys; encryptionKeys += getRecipientKeys(); } return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys, attachedKey); } 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/views/composer/qml/View.qml b/views/composer/qml/View.qml index ef27559b..450193ed 100644 --- a/views/composer/qml/View.qml +++ b/views/composer/qml/View.qml @@ -1,567 +1,567 @@ /* * 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 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 visibleViews: 2 property bool newMessage: false property int loadType: Kube.ComposerController.Draft property variant message: {} property variant recipients: [] property variant accountId: {} resources: [ Kube.ComposerController { id: composerController objectName: "composerController" sign: signCheckbox.checked encrypt: encryptCheckbox.checked onDone: root.done() 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 onMessageLoaded: { textEditor.initialText = body } onCleared: { textEditor.initialText = "" } } ] onSetup: { loadMessage(root.message, root.loadType) if (root.accountId) { composerController.identitySelector.currentAccountId = root.accountId } } onRefresh: { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "mail", "specialPurpose": "drafts"}) //For autocompletion Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "contacts"}) } onAborted: { //Avoid loosing the message if (composerController.saveAsDraftAction.enabled) { composerController.saveAsDraftAction.execute() } } function loadMessage(message, loadType) { 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 * 15 Layout.minimumWidth: Kube.Units.gridUnit * 5 color: Kube.Colors.darkBackgroundColor ColumnLayout { anchors { fill: parent topMargin: Kube.Units.largeSpacing leftMargin: Kube.Units.largeSpacing } spacing: Kube.Units.largeSpacing Kube.PositiveButton { objectName: "newMailButton" width: parent.width - Kube.Units.largeSpacing focus: true text: qsTr("New Email") onClicked: { listView.currentIndex = -1 composerController.clear() subject.forceActiveFocus() } } Kube.Label{ text: qsTr("Drafts") color: Kube.Colors.highlightedTextColor font.weight: Font.Bold } 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, Kube.ComposerController.Draft) } } model: Kube.MailListModel { id: mailListModel showDrafts: true } delegate: Kube.ListDelegate { id: delegateRoot color: Kube.Colors.darkBackgroundColor border.width: 0 Item { id: content anchors { fill: parent margins: Kube.Units.smallSpacing } Kube.Label { width: content.width - Kube.Units.largeSpacing 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 rightMargin: Kube.Units.largeSpacing 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 bottomMargin: Kube.Units.smallSpacing rightMargin: Kube.Units.largeSpacing } 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 font.bold: 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 ? model.filename : "" icon: model.iconname ? model.iconname : "" clip: true actionIcon: Kube.Icons.remove onExecute: composerController.attachments.remove(model.id) } } } RowLayout { spacing: Kube.Units.largeSpacing Row { 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 } Kube.TextButton { id: deleteButton text: qsTr("Remove Formatting") visible: textEditor.htmlEnabled onClicked: textEditor.clearFormatting() } } Item { height: 1 Layout.fillWidth: true } Kube.Button { text: qsTr("Attach file") onClicked: { fileDialog.open() } Dialogs.FileDialog { id: fileDialog title: qsTr("Choose a file to attach") folder: shortcuts.home selectFolder: false selectExisting: true selectMultiple: true onAccepted: { for (var i = 0; i < fileDialog.fileUrls.length; ++i) { composerController.attachments.add({url: fileDialog.fileUrls[i]}) } } } } } Kube.TextEditor { id: textEditor objectName: "textEditor" activeFocusOnTab: true Layout.fillWidth: true Layout.fillHeight: true onHtmlEnabledChanged: { composerController.htmlBody = htmlEnabled; } onActiveFocusChanged: closeFirstSplitIfNecessary() Keys.onEscapePressed: recipients.forceActiveFocus(Qt.TabFocusReason) 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: false + checked: composerController.encrypt } Kube.Label { text: qsTr("encrypt") } } RowLayout { enabled: composerController.foundPersonalKeys Kube.CheckBox { id: signCheckbox - checked: false + 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: root.done() } 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 objectName: "identityCombo" width: parent.width - Kube.Units.largeSpacing * 2 model: composerController.identitySelector.model textRole: "address" Layout.fillWidth: true //A regular binding is not enough in this case, we have to use the Binding element Binding { target: identityCombo; property: "currentIndex"; value: composerController.identitySelector.currentIndex } 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 }