From ba7128b30850594c7efb258d1794e377eede364a Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sun, 1 Jan 2017 13:37:19 +0100 Subject: Instead of using the action system we use controllers only. It's simpler, and the action system was just too complex to use in a typesafe way. --- components/package/contents/ui/FocusComposer.qml | 61 +++--- docs/design.md | 4 +- framework/actions/CMakeLists.txt | 4 +- framework/actions/actionbroker.cpp | 5 + framework/actions/actionbroker.h | 1 + framework/actions/actionhandler.cpp | 17 ++ framework/actions/actionhandler.h | 55 ++++- framework/actions/context.cpp | 34 +++ framework/actions/context.h | 27 ++- framework/actions/tests/CMakeLists.txt | 6 + framework/actions/tests/actiontest.cpp | 102 +++++++++ framework/domain/CMakeLists.txt | 3 + framework/domain/actions/sinkactions.cpp | 74 +------ framework/domain/completer.cpp | 26 +++ framework/domain/completer.h | 40 ++++ framework/domain/composercontroller.cpp | 268 ++++++++++++++--------- framework/domain/composercontroller.h | 118 ++++------ framework/domain/controller.cpp | 55 +++++ framework/domain/controller.h | 75 +++++++ framework/domain/selector.cpp | 26 +++ framework/domain/selector.h | 50 +++++ 21 files changed, 754 insertions(+), 297 deletions(-) create mode 100644 framework/actions/tests/CMakeLists.txt create mode 100644 framework/actions/tests/actiontest.cpp create mode 100644 framework/domain/completer.cpp create mode 100644 framework/domain/completer.h create mode 100644 framework/domain/controller.cpp create mode 100644 framework/domain/controller.h create mode 100644 framework/domain/selector.cpp create mode 100644 framework/domain/selector.h 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 @@ -23,37 +23,26 @@ 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 @@ -91,14 +80,14 @@ Controls2.Popup { 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 } } @@ -116,15 +105,15 @@ Controls2.Popup { 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 } } @@ -141,15 +130,15 @@ Controls2.Popup { 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 } } @@ -161,13 +150,13 @@ Controls2.Popup { 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 } } @@ -201,20 +190,20 @@ Controls2.Popup { 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 @@ -242,7 +231,7 @@ Controls2.Popup { Controls2.Button { text: "Save as Draft" - enabled: saveAsDraftAction.ready + enabled: saveAsDraftAction.enabled onClicked: { saveAsDraftAction.execute() } @@ -251,7 +240,7 @@ Controls2.Popup { 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 @@ -94,7 +94,7 @@ The action can, through property-binding, reevaluate its ready state based on th #### 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. @@ -102,7 +102,7 @@ It is possible that multiple action handlers are avialable for the same action, 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. 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 @@ -9,8 +9,10 @@ set(SRCS 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 @@ -94,3 +94,8 @@ void ActionBroker::registerHandler(const QByteArray &actionId, ActionHandler *ha { 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 @@ -36,6 +36,7 @@ public: 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(); 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 @@ -31,6 +31,11 @@ ActionHandler::ActionHandler(QObject *parent) } +ActionHandler::~ActionHandler() +{ + ActionBroker::instance().unregisterHandler(mActionId, this); +} + bool ActionHandler::isActionReady(Context *context) { if (context) { @@ -67,6 +72,8 @@ ActionResult ActionHandler::execute(Context *context) void ActionHandler::setActionId(const QByteArray &actionId) { + //Reassigning the id is not supported + Q_ASSERT(mActionId.isEmpty()); mActionId = actionId; ActionBroker::instance().registerHandler(actionId, this); } @@ -76,6 +83,16 @@ 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), 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 @@ -24,9 +24,9 @@ #include #include "actionresult.h" +#include "context.h" namespace Kube { -class Context; class ActionHandler : public QObject { @@ -35,6 +35,7 @@ class ActionHandler : public QObject public: ActionHandler(QObject *parent = 0); + virtual ~ActionHandler(); virtual bool isActionReady(Context *context); @@ -43,25 +44,65 @@ public: 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; 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 @@ -29,6 +29,20 @@ Context::Context(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(); @@ -41,6 +55,20 @@ void Context::clear() } } +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"; @@ -55,3 +83,9 @@ QDebug operator<<(QDebug dbg, const Kube::Context &context) 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 @@ -19,17 +19,20 @@ #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 { @@ -38,13 +41,27 @@ 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 @@ -20,6 +20,9 @@ set(mailplugin_SRCS identitiesmodel.cpp recepientautocompletionmodel.cpp settings/accountsettings.cpp + selector.cpp + completer.cpp + controller.cpp ) find_package(KF5 REQUIRED COMPONENTS Package) 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 @@ -16,7 +16,7 @@ Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include + #include #include @@ -78,12 +78,18 @@ static ActionHandlerHelper deleteHandler("org.kde.kube.actions.delete", } ); +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(); @@ -110,65 +116,3 @@ static ActionHandlerHelper sendOutboxHandler("org.kde.kube.actions.sendOutbox", }} ); -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/domain/completer.h b/framework/domain/completer.h new file mode 100644 index 00000000..a672b809 --- /dev/null +++ b/framework/domain/completer.h @@ -0,0 +1,40 @@ +/* + 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 + +class Completer : public QObject { + Q_OBJECT + Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT) + Q_PROPERTY (QString searchString WRITE setSearchString READ searchString) + +public: + Completer(QAbstractItemModel *model); + QAbstractItemModel *model() { return mModel; } + virtual void setSearchString(const QString &s) { mSearchString = s; } + QString searchString() const { return mSearchString; } + +private: + 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 @@ -19,9 +19,6 @@ #include "composercontroller.h" -#include -#include -#include #include #include #include @@ -40,41 +37,9 @@ 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) { } @@ -88,33 +53,72 @@ public: 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) @@ -122,18 +126,20 @@ 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"; @@ -159,68 +165,122 @@ void applyAddresses(const QString &list, std::functionto(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 @@ -23,110 +23,74 @@ #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/domain/controller.cpp b/framework/domain/controller.cpp new file mode 100644 index 00000000..fb971136 --- /dev/null +++ b/framework/domain/controller.cpp @@ -0,0 +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 "controller.h" + +#include +#include + +using namespace Kube; + +ControllerAction::ControllerAction() + : QObject() +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +void ControllerAction::execute() +{ + emit triggered(); +} + +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()); + } +} + +void Controller::run(const KAsync::Job &job) +{ + 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/domain/controller.h b/framework/domain/controller.h new file mode 100644 index 00000000..c152a588 --- /dev/null +++ b/framework/domain/controller.h @@ -0,0 +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_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; } \ + +namespace Kube { + +class ControllerAction : public QObject { + Q_OBJECT + Q_PROPERTY(bool enabled MEMBER mEnabled NOTIFY enabledChanged) +public: + 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; + +public slots: + void clear(); + +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/domain/selector.h b/framework/domain/selector.h new file mode 100644 index 00000000..77c47ba7 --- /dev/null +++ b/framework/domain/selector.h @@ -0,0 +1,50 @@ +/* + 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 + +/** + * 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: + Selector(QAbstractItemModel *model); + + virtual QAbstractItemModel *model() { return mModel; } + + void setCurrentIndex(int i) { + mCurrentIndex = i; + Q_ASSERT(mModel); + setCurrent(mModel->index(mCurrentIndex, 0)); + } + + int currentIndex() { return mCurrentIndex; } + + virtual void setCurrent(const QModelIndex &) = 0; +private: + QAbstractItemModel *mModel = nullptr; + int mCurrentIndex = 0; +}; + -- cgit v1.2.3