diff --git a/components/package/contents/ui/FocusComposer.qml b/components/package/contents/ui/FocusComposer.qml index 07fb08b6..902309a8 100644 --- a/components/package/contents/ui/FocusComposer.qml +++ b/components/package/contents/ui/FocusComposer.qml @@ -1,263 +1,252 @@ /* * 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.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.0 as Controls2 import org.kde.kirigami 1.0 as Kirigami import org.kube.framework.domain 1.0 as KubeFramework -import org.kube.framework.actions 1.0 as KubeAction Controls2.Popup { id: root //Controller KubeFramework.ComposerController { - id: composer + id: composerController onDone: { + clear(); root.close() } } - //context - property variant mailcontext: composer.mailContext - //actions - property variant sendAction: composer.sendAction - property variant saveAsDraftAction: composer.saveAsDraftAction + property variant sendAction: composerController.sendAction + property variant saveAsDraftAction: composerController.saveAsDraftAction //BEGIN functions function loadMessage(message, loadAsDraft) { - composer.loadMessage(message, loadAsDraft) - } - - function saveAsDraft() { - composer.saveAsDraft() - } - - function clear() { - composer.clear(); + composerController.loadMessage(message, loadAsDraft) } //END functions modal: true focus: true closePolicy: Controls2.Popup.CloseOnEscape | Controls2.Popup.CloseOnPressOutsideParent Item { height: parent.height width: parent.width ColumnLayout { anchors { fill: parent margins: Kirigami.Units.largeSpacing } ColumnLayout { anchors.fill: parent GridLayout { columns: 2 Controls2.Label { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight text: "To" } AutocompleteLineEdit { id: to Layout.fillWidth: true - text: mailcontext.to + text: composerController.to onTextChanged: { - mailcontext.to = text; + composerController.to = text; } - model: composer.recipientCompleter.model + model: composerController.recipientCompleter.model onSearchTermChanged: { - composer.recipientCompleter.searchString = searchTerm + composerController.recipientCompleter.searchString = searchTerm } } Controls2.Label { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight text: "Cc" visible: cc.visible } AutocompleteLineEdit { id: cc Layout.fillWidth: true visible: false - text: mailcontext.cc + text: composerController.cc onTextChanged: { - mailcontext.cc = text; + composerController.cc = text; } - model: composer.recipientCompleter.model + model: composerController.recipientCompleter.model onSearchTermChanged: { - composer.recipientCompleter.searchString = searchTerm + composerController.recipientCompleter.searchString = searchTerm } } Controls2.Label { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight text: "Bcc" visible: bcc.visible } AutocompleteLineEdit { id: bcc Layout.fillWidth: true visible : false - text: mailcontext.bcc + text: composerController.bcc onTextChanged: { - mailcontext.bcc = text; + composerController.bcc = text; } - model: composer.recipientCompleter.model + model: composerController.recipientCompleter.model onSearchTermChanged: { - composer.recipientCompleter.searchString = searchTerm + composerController.recipientCompleter.searchString = searchTerm } } Controls2.Label { text: "From" } RowLayout { Controls2.ComboBox { id: identityCombo - model: composer.identitySelector.model + model: composerController.identitySelector.model textRole: "displayName" Layout.fillWidth: true onCurrentIndexChanged: { - composer.identitySelector.currentIndex = currentIndex + composerController.identitySelector.currentIndex = currentIndex } } Controls2.Button { id: ccButton text: "Cc" onClicked: { cc.visible = true ccButton.visible = false } } Controls2.Button { id: bccButton text: "Bcc" onClicked: { bcc.visible = true bccButton.visible = false } } } } Controls2.TextField { id: subject Layout.fillWidth: true placeholderText: "Enter Subject..." - text: mailcontext.subject + text: composerController.subject onTextChanged: { - mailcontext.subject = text; + composerController.subject = text; } } Controls2.TextArea { id: content - text: mailcontext.body + text: composerController.body onTextChanged: { - mailcontext.body = text; + composerController.body = text; } Layout.fillWidth: true Layout.fillHeight: true } RowLayout { id: bottomBar width: parent.width Controls2.Button { text: "Discard" onClicked: { root.close() } } Item { Layout.fillWidth: true } Controls2.Button { text: "Save as Draft" - enabled: saveAsDraftAction.ready + enabled: saveAsDraftAction.enabled onClicked: { saveAsDraftAction.execute() } } Controls2.Button { text: "Send" - enabled: sendAction.ready + enabled: sendAction.enabled onClicked: { sendAction.execute() } } } } } } } diff --git a/docs/design.md b/docs/design.md index 0120ecf5..3b37a5b2 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,298 +1,298 @@ # Architecture / Design ## Overview Kube is supposed to be a small and concise codebase that is easy to modify and evolve. It's following a reactive model, where in one direction we have actions generating modifications, and in the other direction models updating themselves on changes. The overall architecture is split into three layers; Ui, Domain Logic and Infrastructure. ``` +----------------------------+ | UI Components | +----------------------------+ | | | Domain Logic | | Actions/Models | | | +--------------+------+------+ | | | | | Sink |Config| ... | | | | | +--------------+------+------+ ``` The UI Layer consists of views (mostly written in QML), view-models (models that are view specific and potentially implement user interaction details), and the glue code to use various models and actions from the interface. Different UI layers may exist for different form factors. The domain logic layer holds the application state. It povides models to access data and actions to act upon it. The domain logic is by definition Kube specific and not sharable with other applications, as it needs to be taylored exactly according to the requirements of Kube. The infrastructure layer provides: * Data access (Sink) * Configuration (Config files, etc.) * Various functionality provided by libraries (email sending, ldap, iTip handling, iCal implementation (kcalcore), vCard implementation, ...) Various bits of the infrastructure layer may be exchanged on different platforms, to i.e. integrate into native infrastructure providers on a platform. ## UI / Application The UI / Application layer contains all the view components, and their composition, that make up the application. All the interactions between the different components are defined here. ## Components The application consists of various application components. A component could be a maillist, an event-editor or the complete kube-mail application. Each component is instantiable on it's own, and has an API to interact with it. The API i.e. allows to set a folder for the maillist, or an event for the event-editor. Components can be nested (a component can instantiate another component) A component primarily is a QML UI. The QML UI is built on top of: * One or more models that are instantiated to provide the data. * Actions that are instantiated in QML. ## Component interaction The application is made up of various nested components that often need to interact with each other. If we look at the example of the org.kube.mail component: 1. The folderlist-component current-folder property is connected to maillist parentFolder property to display the mails of the currently selected folder. 2. The "add-note" action might either switch to the org.kube.note application as currently displayed component, or it might just display a quick-note widget directly inline. The first usecase can be achieved by the parent component doing a property binding to connect the different components together as desired. The second usecase requires actions to interact with 'a' parent component, but without knowing with which one. Actions can thus be handled by ActionHandlers anywhere in the application. This makes it possible for i.e. a maillist to display a note-widget directly inline, or letting the parent component handle the action to show a full note editor. If nothing handles the action, the root component (the shell)can switch to the note application component. ## Third party users of components Since components are self contained and made available throuh the KPackage sytem, external applications can load fully functional Kube components. For example, the KDE calendar plasmoid could load the Kube Event Viewer component when available, and thus provide Kube's full functionality of that component, including all actions etc, without having to reimplement the Domain Logic (as is the case if only data access is provided through Sink). ## Domain Logic ### Models Self-updating models are used to implement the read-only part of the application. By using QAbstractItemModels we can reuse the existing update mechanism, have something that works well with QML, and avoid implementing a boatload of boilerplate code for hand-coded domain objects. Models should always be reactive and configured with a query, so they are asynchronous. By implementing everything according to that model we can later on achieve lazy-loading of properties and zero-copy (or at least close to zero-copy) directly from storage without modifying anything besides data access. Models are self contained and have an API to set i.e. a query for what to load. Models can load data from anywhere. Typically models are implemented in C++ to interface with the rest of the system, but some models may also be implemented directly in QML. ### Actions An action represents something that can be done, such as "mark as read", "delete", "move somewhere", but also "show this mail" or "give me a composer to write a mail". An action has: * an id (i.e. org.kube.actions.make-as-read) * an ready state (a property for the UI to know when the action can be triggered, that changes depending on the context) * an action context, which contains everything the action needs to execute. * an icon * a name The action context contains the dataset the action works upon plus any additional information that is required. A mark-as-read action for instance only requires the mail-set to work on, while a tag-with action requires any entity (mail, event, ...) and a tag (unless there is one action per tag...). The action can, through property-binding, reevaluate its ready state based on the currently set context that the UI continuously updates through property binding. Context objects can be shared by various actions. #### Pre-action handler A pre-action handler can be used to supply additional context information for the action to execute. This can be used to i.e. retrieve configuration information or resolve a user uid over ldap. -An action can be executed if a set of available pre-action handlers plus the initially supplied informatin can complete the context so the target action-handler can be executed. +An action can be executed if a set of available pre-action handlers plus the initially supplied information can complete the context so the target action-handler can be executed. #### Selecting action handlers out of candidates. It is possible that multiple action handlers are avialable for the same action, i.e. because different accounts supplied an action handler for the same action. In such a case it is necessary to select the right action handler based on the context. A simple criteria could be the currently selected account. #### Automatic action discovery -While in many places explicit instantiation of actions is desirable, sometimes we may want to offer all available actions for a certain type. For this it should be possible to i.e. query for all actions that apply to a mail. That way it is possible to centrally add a new action that automatically becomes available everywhere. Note that this only works for actions that don't require an additional UI, since the components would have to embedd that somewhere. +While in many places explicit instantiation of actions is desirable, sometimes we may want to offer all available actions for a certain type. For this it should be possible to i.e. query for all actions that apply to a mail. That way it is possible to centrally add a new action that automatically become available everywhere. Note that this only works for actions that don't require an additional UI, since the components would have to embed that somewhere. #### Implementation Actions are objects that provide the API, and that QML can instantiate directly with it's id. The C++ implementation looks up the action handler via a broker. * Action: The interface to execute/launch the action. Forwards request and context to broker. * ActionHandler: A handler for a specific action. Registers itself with the broker. * PreActionHandler: A handler that runs before the action and supplies additional information. * ActionBroker: Forwards action requests to handlers. Selects and executes suitable pre-action handlers. * Context: The context containing everything the handler needs to execute the action. ### Controller Controllers are used to interact with the system. The controller is a QObject with a QObject-property every property that should be editable, and a QValidator for every property, so editors can easily be built using property binding while providing property-level validation and feedback. The domain object is exposed as an opaque QVariant that can i.e. be used in an action-context. This way details from the infrastructure layer don't leak to the UI layer Controllers may execute actions or directly interact with infrastructure where suitable. TODO: we need to find a solution for autocompletion for individual properties. This could be something like a plasma-components specific completer class that is supported by a text component (QCompleter only works for widgets). ### Notifications The system will provide notifications from various sources. Notifications could be: * New mails arrived * An error occurred * A synchronization is in progress * ... Notifications can be displayed in various places of the application. ## Infrastructure The infrastructure layer interfaces with the rest of the system. It is the place where we can integrate with various native infrastructure parts. The interface of the infrastructure layer, that is used by the domain logic, may not expose any implementation details of any infrastructure part, to ensure that all infrastructure parts are exchangable. ### Sink Sink is used for primary data access and handles all synchronization. Interactions with Sink involve: * Adding/removing/configuring resources * Triggering synchronization * Querying of data * Creating/Modifying/Removing entities ### Configuration Configuration as traditionally stored in config files in ~/.kde ### Notification Notifications for the system. ### Files Store/Load/Shared stuff (attachments, events, ....) * Additional to the basic store/load stuff that may need further abstraction for mobile platforms beyond what qt provides. * Android Intents/Libpurpose (share with various applications etc). ### Import/Export Same as files? Import/Export calendar data ### RFC implementations * iCal: KCalCore * vCard: KContacts * iTip: extract from kdepim repo * SMTP: based on libcurl ### Cryptography * PGP, PEP * ObjectTreeParser Keyselection, encryption, decryption, signing Probably requires access to identities in some way. see also [Cryptography](cryptography). ### MIME-Message parsing * ObjectTreeParser * KMime ## Testing TBD ## Problems/Notes: * Dynamic switching between various component UI's can be solved using KPackage ## Example usage in QML ``` KubeActions.Action { requestId: "org.kde.kube.mail.reply" onRequest { mail: context.mail } } KubeActions.ActionContext { id: actionContext mail: kubeMailListView.currentMail } KubeActions.ActionHandle { property int progress property bool complete property bool error property string errormessage } KubeActions.Action { id: markAsReadAction action: "org.kde.kube.action.mark-as-read" context: actionContext //execute() returns an ActionHandle } KubeComponents.FolderList { id: kubeFolderListView } KubeComponents.MailList { id: kubeMailListView parentFolder: kubeFolderListView.currentFolder } KubeComponents.MailView { id: kubeMailView mail: kubeMailListView.currentMail } ``` ## Email Domain Logic * Folder list * Folder List Controller * Move mail to folder * Move/Copy/Delete folder * Synchronize folder * Folder List Model * Mixes Sink queries and subqueries (folder list with smart folders) * name * statistics * Mail list * MailListController * Mark as read * Flag as important * Move to trash * MailListModel * subject * date * sender * folder * ThreadModel * thread leader (otherwise like maillist model) * number of mails in thread * Mail Viewer * MailViewController * reply * forward * move to trash * MailModel * subject, date, sender, folder, content, attachments ## Configuration and Accounts Kube is a groupware application, so one of its most important features is being able to work with various remote backends. We live in a world of multiple devies and applications, so it is interesting to share as much state and configuration accross all different devices and applications, which is why we try to store as much of that in the backend. From the perspective of Kube we are working with different "Accounts". Each account represents a different backend, such as your personal IMAP or Kolab server, or a hosted offering such as GMail or Kolab Now. Each of those accounts may interact with various protocols such as imap, smtp, ldap, caldav etc. To add support for a new backend thus means that a new account type has to be added to Kube. An account consists of: * One or more sink resources to access the remote data * A configuration UI in QML that can be embedded in the accounts setup * Potentially custom action handlers if the default action handlers are not sufficient. * A configuration controller to modify and access the data * A set of action pre-handler to supply the configuration to actions ### Configuration Controller The configuraton controller is not only used in the configuration UI to provide the data, but it is also used by the rest of the system to access configuration of this account. This allows the account to retrieve configruation data on a property-by-property basis i.e. from Sink or a local config file. ### Accounts-Plugin The account is supplied as a kpackage based plugin. The plugin is loaded into kube directly from QML. The plugin registers it's configuration controller and potentially actions. Note: We could have a plugin mechanism that discovers account-plugins should that become necessary at some point. ## Application Context Various parts of the system are context sensitive. I.e. the currently selected account affects which transport is used to send an email, or which folders are currently visble. In future iterations that context can be expanded i.e. with projects that affect prioritization of various data items. The application context is globally available, although it may be altered locally. diff --git a/framework/actions/CMakeLists.txt b/framework/actions/CMakeLists.txt index 9cf0acd1..9fc43b9b 100644 --- a/framework/actions/CMakeLists.txt +++ b/framework/actions/CMakeLists.txt @@ -1,16 +1,18 @@ set(SRCS actionplugin.cpp action.cpp actionhandler.cpp actionbroker.cpp actionresult.cpp context.cpp ) add_library(actionplugin SHARED ${SRCS}) -target_link_libraries(actionplugin KF5::Async) +target_link_libraries(actionplugin KF5::Async sink) qt5_use_modules(actionplugin Core Quick Qml) install(TARGETS actionplugin DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/actions) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/actions) + +add_subdirectory(tests) diff --git a/framework/actions/actionbroker.cpp b/framework/actions/actionbroker.cpp index 17145440..f6bfdd8e 100644 --- a/framework/actions/actionbroker.cpp +++ b/framework/actions/actionbroker.cpp @@ -1,96 +1,101 @@ /* 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 "actionbroker.h" #include "context.h" #include "actionhandler.h" #include #include using namespace Kube; SINK_DEBUG_AREA("actionbroker") ActionBroker::ActionBroker(QObject *parent) : QObject(parent) { } ActionBroker &ActionBroker::instance() { static ActionBroker instance; return instance; } bool ActionBroker::isActionReady(const QByteArray &actionId, Context *context, const QList> &preHandler) { if (!context) { return false; } for (const auto handler : preHandler) { if (!handler->isActionReady(context)) { return false; } } for (const auto handler : mHandler.values(actionId)) { if (handler) { if (handler->isActionReady(context)) { return true; } } } return false; } ActionResult ActionBroker::executeAction(const QByteArray &actionId, Context *context, const QList> &preHandler, const QList> &postHandler) { ActionResult result; if (context) { SinkLog() << "Executing action " << actionId; SinkLog() << *context; for (const auto handler : preHandler) { handler->execute(context); } //TODO the main handler should only execute once the pre handler is done for (const auto handler : mHandler.values(actionId)) { if (handler) { result += handler->execute(context); } } //TODO the post handler should only execute once the main handler is done for (const auto handler : postHandler) { handler->execute(context); } } else { SinkWarning() << "Can't execute without context"; result.setDone(); result.setError(1); } return result; } void ActionBroker::registerHandler(const QByteArray &actionId, ActionHandler *handler) { mHandler.insert(actionId, handler); } + +void ActionBroker::unregisterHandler(const QByteArray &actionId, ActionHandler *handler) +{ + mHandler.remove(actionId, handler); +} diff --git a/framework/actions/actionbroker.h b/framework/actions/actionbroker.h index 84678c16..d893a3e7 100644 --- a/framework/actions/actionbroker.h +++ b/framework/actions/actionbroker.h @@ -1,48 +1,49 @@ /* 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 namespace Kube { class Context; class ActionHandler; class ActionResult; class ActionBroker : public QObject { Q_OBJECT public: static ActionBroker &instance(); bool isActionReady(const QByteArray &actionId, Context *context, const QList> &preHandler); ActionResult executeAction(const QByteArray &actionId, Context *context, const QList> &preHandler, const QList> &postHandler); void registerHandler(const QByteArray &actionId, ActionHandler *handler); + void unregisterHandler(const QByteArray &actionId, ActionHandler *handler); Q_SIGNALS: void readyChanged(); private: ActionBroker(QObject *parent = 0); QMultiMap> mHandler; }; } diff --git a/framework/actions/actionhandler.cpp b/framework/actions/actionhandler.cpp index dc9edeca..eb7b3224 100644 --- a/framework/actions/actionhandler.cpp +++ b/framework/actions/actionhandler.cpp @@ -1,134 +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. */ #include "actionhandler.h" #include "context.h" #include "actionbroker.h" #include using namespace Kube; ActionHandler::ActionHandler(QObject *parent) : QObject(parent) { } +ActionHandler::~ActionHandler() +{ + ActionBroker::instance().unregisterHandler(mActionId, this); +} + bool ActionHandler::isActionReady(Context *context) { if (context) { QVariant returnedValue; QMetaObject::invokeMethod(this, "isReady", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, QVariant::fromValue(context))); return returnedValue.toBool(); } else { qWarning() << "The handler didn't get a context"; } return false; } ActionResult ActionHandler::execute(Context *context) { ActionResult result; QVariant returnedValue; qWarning() << "Executing the handler"; if (context) { //The base implementation to call the handler in QML QMetaObject::invokeMethod(this, "handler", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, QVariant::fromValue(context))); //TODO: support async handlers in QML result.setDone(); } else { qWarning() << "The handler didn't get a context"; result.setDone(); result.setError(1); } return result; } void ActionHandler::setActionId(const QByteArray &actionId) { + //Reassigning the id is not supported + Q_ASSERT(mActionId.isEmpty()); mActionId = actionId; ActionBroker::instance().registerHandler(actionId, this); } QByteArray ActionHandler::actionId() const { return mActionId; } +void ActionHandler::setRequiredProperties(const QSet &requiredProperties) +{ + mRequiredProperties = requiredProperties; +} + +QSet ActionHandler::requiredProperties() const +{ + return mRequiredProperties; +} + ActionHandlerHelper::ActionHandlerHelper(const Handler &handler) : ActionHandler(nullptr), handlerFunction(handler) { } ActionHandlerHelper::ActionHandlerHelper(const IsReadyFunction &isReady, const Handler &handler) : ActionHandler(nullptr), isReadyFunction(isReady), handlerFunction(handler) { } ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &isReady, const Handler &handler) : ActionHandler(nullptr), isReadyFunction(isReady), handlerFunction(handler) { setActionId(actionId); } ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &isReady, const JobHandler &handler) : ActionHandler(nullptr), isReadyFunction(isReady), jobHandlerFunction(handler) { setActionId(actionId); } bool ActionHandlerHelper::isActionReady(Context *context) { if (isReadyFunction) { return isReadyFunction(context); } return true; } ActionResult ActionHandlerHelper::execute(Context *context) { ActionResult result; if (handlerFunction) { handlerFunction(context); result.setDone(); } else { jobHandlerFunction(context).syncThen([=](const KAsync::Error &error) { auto modifyableResult = result; if (error) { qWarning() << "Job failed: " << error.errorCode << error.errorMessage; modifyableResult.setError(1); } modifyableResult.setDone(); }).exec(); } return result; } diff --git a/framework/actions/actionhandler.h b/framework/actions/actionhandler.h index 09ed13c6..5ccf0ac7 100644 --- a/framework/actions/actionhandler.h +++ b/framework/actions/actionhandler.h @@ -1,71 +1,112 @@ /* 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 #include #include "actionresult.h" +#include "context.h" namespace Kube { -class Context; class ActionHandler : public QObject { Q_OBJECT Q_PROPERTY(QByteArray actionId READ actionId WRITE setActionId) public: ActionHandler(QObject *parent = 0); + virtual ~ActionHandler(); virtual bool isActionReady(Context *context); virtual ActionResult execute(Context *context); void setActionId(const QByteArray &); QByteArray actionId() const; + void setRequiredProperties(const QSet &requiredProperties); + QSet requiredProperties() const; + private: QByteArray mActionId; + QSet mRequiredProperties; +}; + +template +class ActionHandlerBase : public ActionHandler +{ +public: + ActionHandlerBase(const QByteArray &actionId) + : ActionHandler{} + { + setActionId(actionId); + } + + bool isActionReady(Context *c) Q_DECL_OVERRIDE + { + auto wrapper = ContextType{*c}; + return isActionReady(wrapper); + } + + ActionResult execute(Context *c) Q_DECL_OVERRIDE + { + ActionResult result; + auto wrapper = ContextType{*c}; + execute(wrapper) + .template syncThen([=](const KAsync::Error &error) { + auto modifyableResult = result; + if (error) { + qWarning() << "Job failed: " << error.errorCode << error.errorMessage; + modifyableResult.setError(1); + } + modifyableResult.setDone(); + }).exec(); + return result; + } +protected: + + virtual bool isActionReady(ContextType &) { return true; } + virtual KAsync::Job execute(ContextType &) = 0; }; class ActionHandlerHelper : public ActionHandler { - Q_OBJECT public: - typedef std::function IsReadyFunction; - typedef std::function Handler; - typedef std::function(Context*)> JobHandler; + typedef std::function IsReadyFunction; + typedef std::function Handler; + typedef std::function(Context *)> JobHandler; ActionHandlerHelper(const Handler &); ActionHandlerHelper(const IsReadyFunction &, const Handler &); ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const Handler &); ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const JobHandler &); - bool isActionReady(Context *context) Q_DECL_OVERRIDE; - ActionResult execute(Context *context) Q_DECL_OVERRIDE; + bool isActionReady(Context *) Q_DECL_OVERRIDE; + ActionResult execute(Context *) Q_DECL_OVERRIDE; private: const IsReadyFunction isReadyFunction; const Handler handlerFunction; const JobHandler jobHandlerFunction; }; } diff --git a/framework/actions/context.cpp b/framework/actions/context.cpp index 8f370a0b..45b660a9 100644 --- a/framework/actions/context.cpp +++ b/framework/actions/context.cpp @@ -1,57 +1,91 @@ /* 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 "context.h" #include #include using namespace Kube; Context::Context(QObject *parent) : QObject(parent) { } +Context::Context(const Context &other) + : QObject() +{ + *this = other; +} + +Context &Context::operator=(const Context &other) +{ + for (const auto &p : other.availableProperties()) { + setProperty(p, other.property(p)); + } + return *this; +} + void Context::clear() { auto meta = metaObject(); for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) { auto property = meta->property(i); setProperty(property.name(), QVariant()); } for (const auto &p : dynamicPropertyNames()) { setProperty(p, QVariant()); } } +QSet Context::availableProperties() const +{ + QSet names; + auto meta = metaObject(); + for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) { + auto property = meta->property(i); + names << property.name(); + } + for (const auto &p : dynamicPropertyNames()) { + names << p; + } + return names; +} + QDebug operator<<(QDebug dbg, const Kube::Context &context) { dbg << "Kube::Context {\n"; auto metaObject = context.metaObject(); for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) { auto property = metaObject->property(i); dbg << property.name() << context.property(property.name()) << "\n"; } for (const auto &p : context.dynamicPropertyNames()) { dbg << p << context.property(p) << "\n"; } dbg << "\n}"; return dbg; } + +QDebug operator<<(QDebug dbg, const Kube::ContextWrapper &context) +{ + dbg << context.context; + return dbg; +} diff --git a/framework/actions/context.h b/framework/actions/context.h index 42ae3a93..4207fe12 100644 --- a/framework/actions/context.h +++ b/framework/actions/context.h @@ -1,50 +1,67 @@ /* 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 - #define KUBE_CONTEXT_PROPERTY(TYPE, NAME, LOWERCASENAME) \ public: Q_PROPERTY(TYPE LOWERCASENAME MEMBER m##NAME NOTIFY LOWERCASENAME##Changed) \ + Q_SIGNALS: void LOWERCASENAME##Changed(); \ + private: TYPE m##NAME; + +#define KUBE_CONTEXTWRAPPER_PROPERTY(TYPE, NAME, LOWERCASENAME) \ + public: \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ - void set##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \ - TYPE get##NAME() const { return m##NAME; } \ - Q_SIGNALS: void LOWERCASENAME##Changed(); \ - private: TYPE m##NAME; + void set##NAME(const TYPE &value) { context.setProperty(NAME::name, QVariant::fromValue(value)); } \ + void clear##NAME() { context.setProperty(NAME::name, QVariant{}); } \ + TYPE get##NAME() const { return context.property(NAME::name).value(); } \ namespace Kube { class Context : public QObject { Q_OBJECT public: Context(QObject *parent = 0); + Context(const Context &); + virtual ~Context(){}; + + Context &operator=(const Context &); + virtual void clear(); + + QSet availableProperties() const; +}; + +class ContextWrapper { +public: + ContextWrapper(Context &c) : context{c} {} + Context &context; }; } QDebug operator<<(QDebug dbg, const Kube::Context &); +QDebug operator<<(QDebug dbg, const Kube::ContextWrapper &); Q_DECLARE_METATYPE(Kube::Context*); diff --git a/framework/actions/tests/CMakeLists.txt b/framework/actions/tests/CMakeLists.txt new file mode 100644 index 00000000..af872a3b --- /dev/null +++ b/framework/actions/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +cmake_policy(SET CMP0063 NEW) +add_executable(actiontest actiontest.cpp) +add_test(actiontest sinkactiontest) +qt5_use_modules(actiontest Core Test) +target_link_libraries(actiontest actionplugin) diff --git a/framework/actions/tests/actiontest.cpp b/framework/actions/tests/actiontest.cpp new file mode 100644 index 00000000..a4ec4432 --- /dev/null +++ b/framework/actions/tests/actiontest.cpp @@ -0,0 +1,102 @@ +#include +#include +#include + +#include +#include +#include + +#include + +SINK_DEBUG_AREA("actiontest") + +class HandlerContext : public Kube::Context { + Q_OBJECT + KUBE_CONTEXT_PROPERTY(QString, Property1, property1) + KUBE_CONTEXT_PROPERTY(QString, Property2, property2) +}; + +class HandlerContextWrapper : public Kube::ContextWrapper { + using Kube::ContextWrapper::ContextWrapper; + KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property1, property1) + KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property2, property2) +}; + + + +class Handler : public Kube::ActionHandlerBase +{ +public: + Handler() : Kube::ActionHandlerBase{"org.kde.kube.test.action1"} + {} + + //TODO default implementation checks that all defined properties are available in the context + // bool isReady() override { + // auto accountId = context->property("accountId").value(); + // return !accountId.isEmpty(); + // } + + KAsync::Job execute(HandlerContextWrapper &context) + { + SinkLog() << "Executing action1"; + SinkLog() << context; + executions.append(context.context); + return KAsync::null(); + } + mutable QList executions; +}; + +class Context1 : public Kube::ContextWrapper { + using Kube::ContextWrapper::ContextWrapper; + KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property1, property1) + KUBE_CONTEXTWRAPPER_PROPERTY(QByteArray, Property2, property2) +}; + +class Context2 : public Kube::ContextWrapper { + using Kube::ContextWrapper::ContextWrapper; + KUBE_CONTEXTWRAPPER_PROPERTY(QByteArray, Property2, property2) +}; + + +class ActionTest : public QObject +{ + Q_OBJECT +private slots: + + void initTestCase() + { + } + + void testActionExecution() + { + Handler actionHandler; + + HandlerContext context; + //Kube::Context context; + HandlerContextWrapper{context}.setProperty1(QString("property1")); + context.setProperty("property2", QVariant::fromValue(QString("property2"))); + auto future = Kube::Action("org.kde.kube.test.action1", context).executeWithResult(); + + QTRY_VERIFY(future.isDone()); + QVERIFY(!future.error()); + + QCOMPARE(actionHandler.executions.size(), 1); + QCOMPARE(actionHandler.executions.first().availableProperties().size(), 2); + } + + void testContextCasting() + { + Kube::Context c; + + Context1 context1{c}; + context1.setProperty1("property1"); + context1.setProperty2("property2"); + + auto context2 = Context2{c}; + QCOMPARE(context2.getProperty2(), QByteArray("property2")); + } + +}; + +QTEST_GUILESS_MAIN(ActionTest) +#include "actiontest.moc" diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index 481d5908..bb522416 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -1,36 +1,39 @@ set(mailplugin_SRCS attachmentmodel.cpp mailplugin.cpp maillistmodel.cpp folderlistmodel.cpp actions/sinkactions.cpp objecttreesource.cpp stringhtmlwriter.cpp composercontroller.cpp messageparser.cpp messageparser_new.cpp messageparser_old.cpp mailtemplates.cpp modeltest.cpp retriever.cpp accountfactory.cpp accountscontroller.cpp accountsmodel.cpp outboxmodel.cpp identitiesmodel.cpp recepientautocompletionmodel.cpp settings/accountsettings.cpp + selector.cpp + completer.cpp + controller.cpp ) find_package(KF5 REQUIRED COMPONENTS Package) add_library(mailplugin SHARED ${mailplugin_SRCS}) qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets Test) target_link_libraries(mailplugin actionplugin settingsplugin sink mimetreeparser KF5::MimeTreeParser KF5::Codecs KF5::Package KF5::Async) add_subdirectory(actions/tests) install(TARGETS mailplugin DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) add_subdirectory(mimetreeparser) diff --git a/framework/domain/actions/sinkactions.cpp b/framework/domain/actions/sinkactions.cpp index fd791a91..a2d4c02c 100644 --- a/framework/domain/actions/sinkactions.cpp +++ b/framework/domain/actions/sinkactions.cpp @@ -1,174 +1,118 @@ /* 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 + #include #include #include #include #include SINK_DEBUG_AREA("sinkactions") using namespace Kube; using namespace Sink; using namespace Sink::ApplicationDomain; static ActionHandlerHelper markAsReadHandler("org.kde.kube.actions.mark-as-read", [](Context *context) -> bool { return context->property("mail").isValid(); }, [](Context *context) { auto mail = context->property("mail").value(); if (!mail) { SinkWarning() << "Failed to get the mail mail: " << context->property("mail"); return; } mail->setProperty("unread", false); SinkLog() << "Mark as read " << mail->identifier(); Store::modify(*mail).exec(); } ); static ActionHandlerHelper moveToTrashHandler("org.kde.kube.actions.move-to-trash", [](Context *context) -> bool { return context->property("mail").isValid(); }, [](Context *context) { auto mail = context->property("mail").value(); if (!mail) { SinkWarning() << "Failed to get the mail mail: " << context->property("mail"); return; } mail->setTrash(true); SinkLog() << "Move to trash " << mail->identifier(); Store::modify(*mail).exec(); } ); static ActionHandlerHelper deleteHandler("org.kde.kube.actions.delete", [](Context *context) -> bool { return context->property("mail").isValid(); }, [](Context *context) { auto mail = context->property("mail").value(); if (!mail) { SinkWarning() << "Failed to get the mail mail: " << context->property("mail"); return; } SinkLog() << "Remove " << mail->identifier(); Store::remove(*mail).exec(); } ); +class FolderContext : public Kube::ContextWrapper { + using Kube::ContextWrapper::ContextWrapper; + KUBE_CONTEXTWRAPPER_PROPERTY(Sink::ApplicationDomain::Folder::Ptr, Folder, folder) +}; + static ActionHandlerHelper synchronizeHandler("org.kde.kube.actions.synchronize", [](Context *context) -> bool { return true; }, - [](Context *context) { - if (auto folder = context->property("folder").value()) { + [](Context *context_) { + auto context = FolderContext{*context_}; + if (auto folder = context.getFolder()) { SinkLog() << "Synchronizing folder " << folder->resourceInstanceIdentifier() << folder->identifier(); auto scope = SyncScope().resourceFilter(folder->resourceInstanceIdentifier()).filter(QVariant::fromValue(folder->identifier())); scope.setType(); Store::synchronize(scope).exec(); } else { SinkLog() << "Synchronizing all"; Store::synchronize(SyncScope()).exec(); } } ); static ActionHandlerHelper sendOutboxHandler("org.kde.kube.actions.sendOutbox", [](Context *context) -> bool { return true; }, ActionHandlerHelper::JobHandler{[](Context *context) -> KAsync::Job { using namespace Sink::ApplicationDomain; Query query; query.containsFilter(ResourceCapabilities::Mail::transport); return Store::fetchAll(query) .each([=](const SinkResource::Ptr &resource) -> KAsync::Job { return Store::synchronize(SyncScope{}.resourceFilter(resource->identifier())); }); }} ); -static ActionHandlerHelper sendMailHandler("org.kde.kube.actions.sendmail", - [](Context *context) -> bool { - auto accountId = context->property("accountId").value(); - return !accountId.isEmpty(); - }, - ActionHandlerHelper::JobHandler{[](Context *context) -> KAsync::Job { - auto accountId = context->property("accountId").value(); - auto message = context->property("message").value(); - SinkLog() << "Sending a mail: " << *context; - - Query query; - query.containsFilter(ApplicationDomain::ResourceCapabilities::Mail::transport); - query.filter(accountId); - return Store::fetchAll(query) - .then>([=](const QList &resources) -> KAsync::Job { - if (!resources.isEmpty()) { - auto resourceId = resources[0]->identifier(); - SinkTrace() << "Sending message via resource: " << resourceId; - Mail mail(resourceId); - mail.setBlobProperty("mimeMessage", message->encodedContent()); - return Store::create(mail); - } - SinkWarning() << "Failed to find a mailtransport resource"; - return KAsync::error(0, "Failed to find a MailTransport resource."); - }); - }} -); - -static ActionHandlerHelper saveAsDraft("org.kde.kube.actions.save-as-draft", - [](Context *context) -> bool { - auto accountId = context->property("accountId").value(); - return !accountId.isEmpty(); - }, - ActionHandlerHelper::JobHandler([](Context *context) -> KAsync::Job { - SinkLog() << "Executing the save-as-draft action"; - SinkLog() << *context; - const auto accountId = context->property("accountId").value(); - const auto message = context->property("message").value(); - auto existingMail = context->property("existingMail").value(); - if (!message) { - SinkWarning() << "Failed to get the mail: " << context->property("mail"); - return KAsync::error(1, "Failed to get the mail: " + context->property("mail").toString()); - } - - if (existingMail.identifier().isEmpty()) { - Query query; - query.containsFilter(ApplicationDomain::ResourceCapabilities::Mail::drafts); - query.filter(accountId); - return Store::fetchOne(query) - .then([=](const SinkResource &resource) -> KAsync::Job { - Mail mail(resource.identifier()); - mail.setDraft(true); - mail.setMimeMessage(message->encodedContent()); - return Store::create(mail); - }); - } else { - SinkWarning() << "Modifying an existing mail" << existingMail.identifier(); - existingMail.setMimeMessage(message->encodedContent()); - return Store::modify(existingMail); - } - }) -); diff --git a/framework/domain/completer.cpp b/framework/domain/completer.cpp new file mode 100644 index 00000000..cacb4faa --- /dev/null +++ b/framework/domain/completer.cpp @@ -0,0 +1,26 @@ +/* + Copyright (c) 2016 Christian Mollekofp + + 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 "completer.h" + +#include + +Completer::Completer(QAbstractItemModel *model) : mModel{model} +{ + QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership); +} diff --git a/framework/actions/actionbroker.h b/framework/domain/completer.h similarity index 52% copy from framework/actions/actionbroker.h copy to framework/domain/completer.h index 84678c16..a672b809 100644 --- a/framework/actions/actionbroker.h +++ b/framework/domain/completer.h @@ -1,48 +1,40 @@ /* - Copyright (c) 2016 Christian Mollekopf + Copyright (c) 2016 Christian Mollekofp 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 +#include +#include -namespace Kube { -class Context; -class ActionHandler; -class ActionResult; - -class ActionBroker : public QObject -{ +class Completer : public QObject { Q_OBJECT -public: - static ActionBroker &instance(); - - bool isActionReady(const QByteArray &actionId, Context *context, const QList> &preHandler); - ActionResult executeAction(const QByteArray &actionId, Context *context, const QList> &preHandler, const QList> &postHandler); + Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT) + Q_PROPERTY (QString searchString WRITE setSearchString READ searchString) - void registerHandler(const QByteArray &actionId, ActionHandler *handler); - -Q_SIGNALS: - void readyChanged(); +public: + Completer(QAbstractItemModel *model); + QAbstractItemModel *model() { return mModel; } + virtual void setSearchString(const QString &s) { mSearchString = s; } + QString searchString() const { return mSearchString; } private: - ActionBroker(QObject *parent = 0); - QMultiMap> mHandler; + QAbstractItemModel *mModel = nullptr; + QString mSearchString; }; -} diff --git a/framework/domain/composercontroller.cpp b/framework/domain/composercontroller.cpp index 57d386c6..4ce356a9 100644 --- a/framework/domain/composercontroller.cpp +++ b/framework/domain/composercontroller.cpp @@ -1,226 +1,286 @@ /* Copyright (c) 2016 Michael Bohlender 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 #include #include "accountsmodel.h" #include "identitiesmodel.h" #include "recepientautocompletionmodel.h" #include "mailtemplates.h" SINK_DEBUG_AREA("composercontroller"); -Q_DECLARE_METATYPE(KMime::Types::Mailbox) - -ComposerController::ComposerController(QObject *parent) : QObject(parent) -{ - QQmlEngine::setObjectOwnership(&mContext, QQmlEngine::CppOwnership); -} - - -Kube::Context* ComposerController::mailContext() -{ - return &mContext; -} - -class RecipientCompleter : public Completer { -public: - RecipientCompleter() : Completer(new RecipientAutocompletionModel) - { - } - - void setSearchString(const QString &s) { - static_cast(model())->setFilter(s); - Completer::setSearchString(s); - } -}; - -Completer *ComposerController::recipientCompleter() const -{ - static auto selector = new RecipientCompleter(); - QQmlEngine::setObjectOwnership(selector, QQmlEngine::CppOwnership); - return selector; -} - class IdentitySelector : public Selector { public: - IdentitySelector(ComposerContext &context) : Selector(new IdentitiesModel), mContext(context) + 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; - mContext.setProperty("identity", QVariant::fromValue(mb)); - mContext.setProperty("accountId", QVariant::fromValue(currentAccountId)); + mController.setIdentity(mb); + mController.setAccountId(currentAccountId); } else { SinkWarning() << "No valid identity for index: " << index; - mContext.setProperty("identity", QVariant{}); - mContext.setProperty("accountId", QVariant{}); + mController.clearIdentity(); + mController.clearAccountId(); } } private: - ComposerContext &mContext; + ComposerController &mController; }; +class RecipientCompleter : public Completer { +public: + RecipientCompleter() : Completer(new RecipientAutocompletionModel) + { + } + + void setSearchString(const QString &s) { + static_cast(model())->setFilter(s); + Completer::setSearchString(s); + } +}; + + +ComposerController::ComposerController() + : Kube::Controller(), + mSendAction{new Kube::ControllerAction}, + mSaveAsDraftAction{new Kube::ControllerAction}, + mRecipientCompleter{new RecipientCompleter}, + mIdentitySelector{new IdentitySelector{*this}} +{ + QObject::connect(mSaveAsDraftAction.data(), &Kube::ControllerAction::triggered, this, &ComposerController::saveAsDraft); + updateSaveAsDraftAction(); + // mSendAction->monitorProperty(); + // mSendAction->monitorProperty([] (const QString &) -> bool{ + // //validate + // }); + // registerAction(&ComposerController::send); + // actionDepends(); + // TODO do in constructor + QObject::connect(mSendAction.data(), &Kube::ControllerAction::triggered, this, &ComposerController::send); + + QObject::connect(this, &ComposerController::toChanged, &ComposerController::updateSendAction); + QObject::connect(this, &ComposerController::subjectChanged, &ComposerController::updateSendAction); + updateSendAction(); +} + +Completer *ComposerController::recipientCompleter() const +{ + return mRecipientCompleter.data(); +} + Selector *ComposerController::identitySelector() const { - static auto selector = new IdentitySelector(*const_cast(&mContext)); - QQmlEngine::setObjectOwnership(selector, QQmlEngine::CppOwnership); - return selector; + return mIdentitySelector.data(); } void ComposerController::setMessage(const KMime::Message::Ptr &msg) { - mContext.setTo(msg->to(true)->asUnicodeString()); - mContext.setCc(msg->cc(true)->asUnicodeString()); - mContext.setSubject(msg->subject(true)->asUnicodeString()); - mContext.setBody(msg->body()); - mContext.setProperty("existingMessage", QVariant::fromValue(msg)); + setTo(msg->to(true)->asUnicodeString()); + setCc(msg->cc(true)->asUnicodeString()); + setSubject(msg->subject(true)->asUnicodeString()); + setBody(msg->body()); + setExistingMessage(msg); } void ComposerController::loadMessage(const QVariant &message, bool loadAsDraft) { Sink::Query query(*message.value()); query.request(); Sink::Store::fetchOne(query).syncThen([this, loadAsDraft](const Sink::ApplicationDomain::Mail &mail) { - mContext.setProperty("existingMail", QVariant::fromValue(mail)); + setExistingMail(mail); + + //TODO this should probably happen as reaction to the property being set. const auto mailData = KMime::CRLFtoLF(mail.getMimeMessage()); if (!mailData.isEmpty()) { KMime::Message::Ptr mail(new KMime::Message); mail->setContent(mailData); mail->parse(); if (loadAsDraft) { + setMessage(mail); + } else { auto reply = MailTemplates::reply(mail); //We assume reply setMessage(reply); - } else { - setMessage(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); } } void applyAddresses(const QString &list, std::function callback) { for (const auto &to : KEmailAddress::splitAddressList(list)) { QByteArray displayName; QByteArray addrSpec; QByteArray comment; KEmailAddress::splitAddress(to.toUtf8(), displayName, addrSpec, comment); callback(addrSpec, displayName); } } -void ComposerController::clear() +Kube::ControllerAction* ComposerController::saveAsDraftAction() +{ + return mSaveAsDraftAction.data(); +} + +Kube::ControllerAction* ComposerController::sendAction() { - mContext.clear(); + return mSendAction.data(); } +KMime::Message::Ptr ComposerController::assembleMessage() +{ + auto mail = mExistingMessage; + if (!mail) { + mail = KMime::Message::Ptr::create(); + } + applyAddresses(getTo(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { + mail->to(true)->addAddress(addrSpec, displayName); + recordForAutocompletion(addrSpec, displayName); + }); + applyAddresses(getCc(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { + mail->cc(true)->addAddress(addrSpec, displayName); + recordForAutocompletion(addrSpec, displayName); + }); + applyAddresses(getBcc(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { + mail->bcc(true)->addAddress(addrSpec, displayName); + recordForAutocompletion(addrSpec, displayName); + }); + + mail->from(true)->addAddress(getIdentity()); + + mail->subject(true)->fromUnicodeString(getSubject(), "utf-8"); + mail->setBody(getBody().toUtf8()); + mail->assemble(); + return mail; +} -Kube::ActionHandler *ComposerController::messageHandler() +void ComposerController::updateSendAction() { - return new Kube::ActionHandlerHelper( - [](Kube::Context *context) { - auto identity = context->property("identity"); - return identity.isValid(); - }, - [this](Kube::Context *context) { - auto mail = context->property("existingMessage").value(); - if (!mail) { - mail = KMime::Message::Ptr::create(); + auto enabled = !getTo().isEmpty() && !getSubject().isEmpty(); + mSendAction->setEnabled(enabled); +} + +void ComposerController::send() +{ + // verify() + // && verify(); + auto message = assembleMessage(); + + auto accountId = getAccountId(); + //SinkLog() << "Sending a mail: " << *this; + using namespace Sink; + using namespace Sink::ApplicationDomain; + + Query query; + query.containsFilter(ApplicationDomain::ResourceCapabilities::Mail::transport); + query.filter(accountId); + auto job = Store::fetchAll(query) + .then>([=](const QList &resources) -> KAsync::Job { + if (!resources.isEmpty()) { + auto resourceId = resources[0]->identifier(); + SinkTrace() << "Sending message via resource: " << resourceId; + Mail mail(resourceId); + mail.setBlobProperty("mimeMessage", message->encodedContent()); + return Store::create(mail); } - applyAddresses(context->property(ComposerContext::To::name).toString(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { - mail->to(true)->addAddress(addrSpec, displayName); - recordForAutocompletion(addrSpec, displayName); - }); - applyAddresses(context->property(ComposerContext::Cc::name).toString(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { - mail->cc(true)->addAddress(addrSpec, displayName); - recordForAutocompletion(addrSpec, displayName); - }); - applyAddresses(context->property(ComposerContext::Bcc::name).toString(), [&](const QByteArray &addrSpec, const QByteArray &displayName) { - mail->bcc(true)->addAddress(addrSpec, displayName); - recordForAutocompletion(addrSpec, displayName); - }); - - mail->from(true)->addAddress(context->property("identity").value()); - - mail->subject(true)->fromUnicodeString(context->property(ComposerContext::Subject::name).toString(), "utf-8"); - mail->setBody(context->property(ComposerContext::Body::name).toString().toUtf8()); - mail->assemble(); - - context->setProperty("message", QVariant::fromValue(mail)); - } - ); + return KAsync::error(0, "Failed to find a MailTransport resource."); + }); + run(job); + job = job.syncThen([&] { + emit done(); + }); } -Kube::Action* ComposerController::saveAsDraftAction() +void ComposerController::updateSaveAsDraftAction() { - auto action = new Kube::Action("org.kde.kube.actions.save-as-draft", mContext); - action->addPreHandler(messageHandler()); - action->addPostHandler(new Kube::ActionHandlerHelper( - [this](Kube::Context *context) { - emit done(); - })); - return action; + mSendAction->setEnabled(true); } -Kube::Action* ComposerController::sendAction() +void ComposerController::saveAsDraft() { - auto action = new Kube::Action("org.kde.kube.actions.sendmail", mContext); - // action->addPreHandler(identityHandler()); - action->addPreHandler(messageHandler()); - // action->addPreHandler(encryptionHandler()); - action->addPostHandler(new Kube::ActionHandlerHelper( - [this](Kube::Context *context) { - emit done(); - })); - return action; + const auto accountId = getAccountId(); + auto existingMail = getExistingMail(); + + auto message = assembleMessage(); + //FIXME this is something for the validation + if (!message) { + SinkWarning() << "Failed to get the mail: "; + return; + // return KAsync::error(1, "Failed to get the mail."); + } + + using namespace Sink; + using namespace Sink::ApplicationDomain; + + auto job = [&] { + if (existingMail.identifier().isEmpty()) { + Query query; + query.containsFilter(ApplicationDomain::ResourceCapabilities::Mail::drafts); + query.filter(accountId); + return Store::fetchOne(query) + .then([=](const SinkResource &resource) -> KAsync::Job { + Mail mail(resource.identifier()); + mail.setDraft(true); + mail.setMimeMessage(message->encodedContent()); + return Store::create(mail); + }); + } else { + SinkWarning() << "Modifying an existing mail" << existingMail.identifier(); + existingMail.setDraft(true); + existingMail.setMimeMessage(message->encodedContent()); + return Store::modify(existingMail); + } + }(); + job = job.syncThen([&] { + emit done(); + }); + run(job); } diff --git a/framework/domain/composercontroller.h b/framework/domain/composercontroller.h index 3e701ed1..c5046306 100644 --- a/framework/domain/composercontroller.h +++ b/framework/domain/composercontroller.h @@ -1,132 +1,96 @@ /* Copyright (c) 2016 Michael Bohlender 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 #include -#include -#include #include +#include -#include -#include +#include "completer.h" +#include "selector.h" +#include "controller.h" -namespace KMime { -class Message; +inline bool operator !=(const KMime::Types::Mailbox &l, const KMime::Types::Mailbox &r) +{ + return !(l.prettyAddress() == r.prettyAddress()); } -class ComposerContext : public Kube::Context { - Q_OBJECT - KUBE_CONTEXT_PROPERTY(QString, To, to) - KUBE_CONTEXT_PROPERTY(QString, Cc, cc) - KUBE_CONTEXT_PROPERTY(QString, Bcc, bcc) - KUBE_CONTEXT_PROPERTY(QString, From, from) - KUBE_CONTEXT_PROPERTY(QString, Subject, subject) - KUBE_CONTEXT_PROPERTY(QString, Body, body) -}; - -class Completer : public QObject { - Q_OBJECT - Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT) - Q_PROPERTY (QString searchString WRITE setSearchString READ searchString) +Q_DECLARE_METATYPE(KMime::Types::Mailbox); -public: - Completer(QAbstractItemModel *model) : mModel{model} - { - QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership); - } - QAbstractItemModel *model() { return mModel; } - virtual void setSearchString(const QString &s) { mSearchString = s; } - QString searchString() const { return mSearchString; } - -private: - QAbstractItemModel *mModel = nullptr; - QString mSearchString; -}; +namespace KMime { +class Message; +} -/** - * Exposes a model and maintains a current index selection. - */ -class Selector : public QObject { +class ComposerController : public Kube::Controller +{ Q_OBJECT - Q_PROPERTY (int currentIndex READ currentIndex WRITE setCurrentIndex) - Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT) - -public: - Selector(QAbstractItemModel *model) : mModel{model} - { - QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership); - } - - virtual QAbstractItemModel *model() { return mModel; } - - void setCurrentIndex(int i) { - mCurrentIndex = i; - Q_ASSERT(mModel); - setCurrent(mModel->index(mCurrentIndex, 0)); - } - int currentIndex() { return mCurrentIndex; } + //Interface properties + KUBE_CONTROLLER_PROPERTY(QString, To, to) + KUBE_CONTROLLER_PROPERTY(QString, Cc, cc) + KUBE_CONTROLLER_PROPERTY(QString, Bcc, bcc) + KUBE_CONTROLLER_PROPERTY(QString, Subject, subject) + KUBE_CONTROLLER_PROPERTY(QString, Body, body) - virtual void setCurrent(const QModelIndex &) = 0; -private: - QAbstractItemModel *mModel = nullptr; - int mCurrentIndex = 0; -}; + //Set by identitySelector + KUBE_CONTROLLER_PROPERTY(KMime::Types::Mailbox, Identity, identity) + KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId) -class ComposerController : public QObject -{ - Q_OBJECT - Q_PROPERTY (Kube::Context* mailContext READ mailContext CONSTANT) + //Set by loadMessage + KUBE_CONTROLLER_PROPERTY(KMime::Message::Ptr, ExistingMessage, existingMessage) + KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Mail, ExistingMail, existingMail) Q_PROPERTY (Completer* recipientCompleter READ recipientCompleter CONSTANT) Q_PROPERTY (Selector* identitySelector READ identitySelector CONSTANT) + //Q_PROPERTY (QValidator* subjectValidator READ subjectValidator CONSTANT) - Q_PROPERTY (Kube::Action* sendAction READ sendAction) - Q_PROPERTY (Kube::Action* saveAsDraftAction READ saveAsDraftAction) + Q_PROPERTY (Kube::ControllerAction* sendAction READ sendAction CONSTANT) + Q_PROPERTY (Kube::ControllerAction* saveAsDraftAction READ saveAsDraftAction CONSTANT) public: - explicit ComposerController(QObject *parent = Q_NULLPTR); - - Kube::Context* mailContext(); + explicit ComposerController(); Completer *recipientCompleter() const; Selector *identitySelector() const; Q_INVOKABLE void loadMessage(const QVariant &draft, bool loadAsDraft); - Kube::Action* sendAction(); - Kube::Action* saveAsDraftAction(); - -public slots: - void clear(); + Kube::ControllerAction* sendAction(); + Kube::ControllerAction* saveAsDraftAction(); -signals: - void done(); +private slots: + void updateSendAction(); + void send(); + void updateSaveAsDraftAction(); + void saveAsDraft(); private: - Kube::ActionHandler *messageHandler(); void recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName); void setMessage(const QSharedPointer &msg); + KMime::Message::Ptr assembleMessage(); - ComposerContext mContext; + QScopedPointer mSendAction; + QScopedPointer mSaveAsDraftAction; + QScopedPointer mRecipientCompleter; + QScopedPointer mIdentitySelector; }; diff --git a/framework/actions/context.cpp b/framework/domain/controller.cpp similarity index 66% copy from framework/actions/context.cpp copy to framework/domain/controller.cpp index 8f370a0b..fb971136 100644 --- a/framework/actions/context.cpp +++ b/framework/domain/controller.cpp @@ -1,57 +1,55 @@ /* 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 "context.h" +#include "controller.h" -#include +#include #include using namespace Kube; -Context::Context(QObject *parent) - : QObject(parent) +ControllerAction::ControllerAction() + : QObject() { + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); +} +void ControllerAction::execute() +{ + emit triggered(); } -void Context::clear() +void Controller::clear() { auto meta = metaObject(); for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) { auto property = meta->property(i); setProperty(property.name(), QVariant()); } for (const auto &p : dynamicPropertyNames()) { setProperty(p, QVariant()); } } -QDebug operator<<(QDebug dbg, const Kube::Context &context) +void Controller::run(const KAsync::Job &job) { - dbg << "Kube::Context {\n"; - auto metaObject = context.metaObject(); - for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) { - auto property = metaObject->property(i); - dbg << property.name() << context.property(property.name()) << "\n"; - } - for (const auto &p : context.dynamicPropertyNames()) { - dbg << p << context.property(p) << "\n"; - } - dbg << "\n}"; - return dbg; + auto jobToExec = job; + //TODO handle error + //TODO attach a log context to the execution that we can gather from the job? + jobToExec.exec(); } diff --git a/framework/actions/context.h b/framework/domain/controller.h similarity index 61% copy from framework/actions/context.h copy to framework/domain/controller.h index 42ae3a93..c152a588 100644 --- a/framework/actions/context.h +++ b/framework/domain/controller.h @@ -1,50 +1,75 @@ /* 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_CONTEXT_PROPERTY(TYPE, NAME, LOWERCASENAME) \ +#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; } \ - Q_SIGNALS: void LOWERCASENAME##Changed(); \ - private: TYPE m##NAME; - namespace Kube { -class Context : public QObject { +class ControllerAction : public QObject { Q_OBJECT + Q_PROPERTY(bool enabled MEMBER mEnabled NOTIFY enabledChanged) public: - Context(QObject *parent = 0); - virtual ~Context(){}; - virtual void clear(); + ControllerAction(); + ~ControllerAction() = default; + + Q_INVOKABLE void execute(); + void setEnabled(bool enabled) { setProperty("enabled", enabled); } + +signals: + void enabledChanged(); + void triggered(); + +private: + bool mEnabled = true; }; -} +class Controller : public QObject { + Q_OBJECT +public: + Controller() = default; + virtual ~Controller() = default; -QDebug operator<<(QDebug dbg, const Kube::Context &); +public slots: + void clear(); -Q_DECLARE_METATYPE(Kube::Context*); +signals: + void done(); + void error(); +protected: + void run(const KAsync::Job &job); +}; + +} diff --git a/framework/domain/selector.cpp b/framework/domain/selector.cpp new file mode 100644 index 00000000..ddb23744 --- /dev/null +++ b/framework/domain/selector.cpp @@ -0,0 +1,26 @@ +/* + Copyright (c) 2016 Christian Mollekofp + + 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 "selector.h" + +#include + +Selector::Selector(QAbstractItemModel *model) : mModel{model} +{ + QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership); +} diff --git a/framework/actions/actionbroker.h b/framework/domain/selector.h similarity index 52% copy from framework/actions/actionbroker.h copy to framework/domain/selector.h index 84678c16..77c47ba7 100644 --- a/framework/actions/actionbroker.h +++ b/framework/domain/selector.h @@ -1,48 +1,50 @@ /* - Copyright (c) 2016 Christian Mollekopf + Copyright (c) 2016 Christian Mollekofp 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 +#include -namespace Kube { -class Context; -class ActionHandler; -class ActionResult; - -class ActionBroker : public QObject -{ +/** + * Exposes a model and maintains a current index selection. + */ +class Selector : public QObject { Q_OBJECT + Q_PROPERTY (int currentIndex READ currentIndex WRITE setCurrentIndex) + Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT) + public: - static ActionBroker &instance(); + Selector(QAbstractItemModel *model); - bool isActionReady(const QByteArray &actionId, Context *context, const QList> &preHandler); - ActionResult executeAction(const QByteArray &actionId, Context *context, const QList> &preHandler, const QList> &postHandler); + virtual QAbstractItemModel *model() { return mModel; } - void registerHandler(const QByteArray &actionId, ActionHandler *handler); + void setCurrentIndex(int i) { + mCurrentIndex = i; + Q_ASSERT(mModel); + setCurrent(mModel->index(mCurrentIndex, 0)); + } -Q_SIGNALS: - void readyChanged(); + int currentIndex() { return mCurrentIndex; } + virtual void setCurrent(const QModelIndex &) = 0; private: - ActionBroker(QObject *parent = 0); - QMultiMap> mHandler; + QAbstractItemModel *mModel = nullptr; + int mCurrentIndex = 0; }; -}