From 6eceaf5795dbaf60d6081824889504e89dac20f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Vr=C3=A1til?= <daniel.vratil@kdab.com> Date: Thu, 24 Mar 2016 18:20:33 +0100 Subject: [PATCH 37/47] Extend ETM/ETV API to make drag&drop popup menu customizable Users can now add custom actions to drag&drop popup menu by calling ETV::addCustomDropAction(). When users activates the custom action the ETM::customDropAction() signal is emitted. User can then handle the action manually or just transform the dropped content and let ETM to finish the drop. --- akonadi/dragdropmanager.cpp | 64 ++++++++++++++++++++---- akonadi/dragdropmanager_p.h | 18 +++++++ akonadi/entitytreemodel.cpp | 33 +++++++++++++ akonadi/entitytreemodel.h | 49 +++++++++++++++++++ akonadi/entitytreemodel_p.cpp | 7 +++ akonadi/entitytreemodel_p.h | 2 + akonadi/entitytreeview.cpp | 12 +++++ akonadi/entitytreeview.h | 39 +++++++++++++++ akonadi/pastehelper.cpp | 84 +++++++++++++++++--------------- akonadi/pastehelper_p.h | 47 ++++++++++++++++++ akonadi/tests/actionstatemanagertest.cpp | 2 +- 11 files changed, 309 insertions(+), 48 deletions(-) diff --git a/akonadi/dragdropmanager.cpp b/akonadi/dragdropmanager.cpp index e0641bb29..9db56099a 100644 --- a/akonadi/dragdropmanager.cpp +++ b/akonadi/dragdropmanager.cpp @@ -208,27 +208,51 @@ bool DragDropManager::processDropEvent(QDropEvent *event, bool &menuCanceled, bo // otherwise show up a menu to allow the user to select an action QMenu popup(m_view); - QAction *moveDropAction = 0; - QAction *copyDropAction = 0; - QAction *linkAction = 0; + QList<QAction *> moveDropActions; + QList<QAction *> copyDropActions; + QList<QAction *> linkActions; QString sequence; if (moveAllowed) { sequence = QKeySequence(Qt::ShiftModifier).toString(); sequence.chop(1); // chop superfluous '+' - moveDropAction = popup.addAction(KIcon(QString::fromLatin1("go-jump")), i18n("&Move Here") + QLatin1Char('\t') + sequence); + moveDropActions << popup.addAction(KIcon(QString::fromLatin1("go-jump")), i18n("&Move Here") + QLatin1Char('\t') + sequence); + Q_FOREACH (const CustomAction &ca, mCustomActions) { + if (ca.dropAction != Qt::MoveAction) { + continue; + } + QAction *action = popup.addAction(ca.icon, ca.text); + action->setProperty("customActionId", ca.id); + moveDropActions << action; + } } if (copyAllowed) { sequence = QKeySequence(Qt::ControlModifier).toString(); sequence.chop(1); // chop superfluous '+' - copyDropAction = popup.addAction(KIcon(QString::fromLatin1("edit-copy")), i18n("&Copy Here") + QLatin1Char('\t') + sequence); + copyDropActions << popup.addAction(KIcon(QString::fromLatin1("edit-copy")), i18n("&Copy Here") + QLatin1Char('\t') + sequence); + Q_FOREACH (const CustomAction &ca, mCustomActions) { + if (ca.dropAction != Qt::CopyAction) { + continue; + } + QAction *action = popup.addAction(ca.icon, ca.text); + action->setProperty("customActionId", ca.id); + copyDropActions << action; + } } if (linkAllowed) { sequence = QKeySequence(Qt::ControlModifier + Qt::ShiftModifier).toString(); sequence.chop(1); // chop superfluous '+' - linkAction = popup.addAction(KIcon(QLatin1String("edit-link")), i18n("&Link Here") + QLatin1Char('\t') + sequence); + linkActions << popup.addAction(KIcon(QLatin1String("edit-link")), i18n("&Link Here") + QLatin1Char('\t') + sequence); + Q_FOREACH (const CustomAction &ca, mCustomActions) { + if (ca.dropAction != Qt::LinkAction) { + continue; + } + QAction *action = popup.addAction(ca.icon, ca.text); + action->setProperty("customActionId", ca.id); + linkActions << action; + } } popup.addSeparator(); @@ -238,16 +262,27 @@ bool DragDropManager::processDropEvent(QDropEvent *event, bool &menuCanceled, bo if (!activatedAction) { menuCanceled = true; return false; - } else if (activatedAction == moveDropAction) { + } else if (moveDropActions.contains(activatedAction)) { event->setDropAction(Qt::MoveAction); - } else if (activatedAction == copyDropAction) { + } else if (copyDropActions.contains(activatedAction)) { event->setDropAction(Qt::CopyAction); - } else if (activatedAction == linkAction) { + } else if (linkActions.contains(activatedAction)) { event->setDropAction(Qt::LinkAction); } else { menuCanceled = true; return false; } + + // This is a custom action provided by user; we need to encode it into + // the QMimeData object so that model can correctly trigger user's handler + const QVariant customActionId = activatedAction->property("customActionId"); + if (customActionId.isValid()) { + // FIXME: I know this is super-bad thing to do, but there's no other way + // to pass this information to the receiving model :( + QMimeData *md = const_cast<QMimeData*>(data); + md->setProperty("customActionId", customActionId); + } + return true; } @@ -331,3 +366,14 @@ void DragDropManager::setManualSortingActive(bool active) { mIsManualSortingActive = active; } + +void DragDropManager::addCustomAction(const QString &id, const KIcon &icon, + const QString &text, Qt::DropAction dropAction) +{ + CustomAction ca; + ca.id = id; + ca.icon = icon; + ca.text = text; + ca.dropAction = dropAction; + mCustomActions << ca; +} diff --git a/akonadi/dragdropmanager_p.h b/akonadi/dragdropmanager_p.h index 0151c4d6a..c189feade 100644 --- a/akonadi/dragdropmanager_p.h +++ b/akonadi/dragdropmanager_p.h @@ -21,9 +21,14 @@ #define AKONADI_DRAGDROPMANAGER_P_H #include <QAbstractItemView> +#include <QVector> #include "akonadi/collection.h" +#include <KIcon> + +class QObject; + namespace Akonadi { @@ -71,6 +76,9 @@ public: */ void setManualSortingActive(bool active); + void addCustomAction(const QString &id, const KIcon &icon, const QString &text, + Qt::DropAction dropAction); + private: Collection currentDropTarget(QDropEvent *event) const; @@ -78,6 +86,16 @@ private: bool mShowDropActionMenu; bool mIsManualSortingActive; QAbstractItemView *m_view; + + struct CustomAction { + CustomAction() : dropAction(Qt::IgnoreAction) {} + + QString id; + KIcon icon; + QString text; + Qt::DropAction dropAction; + }; + QVector<CustomAction> mCustomActions; }; } diff --git a/akonadi/entitytreemodel.cpp b/akonadi/entitytreemodel.cpp index a2e8f2d4a..046c83d69 100644 --- a/akonadi/entitytreemodel.cpp +++ b/akonadi/entitytreemodel.cpp @@ -39,6 +39,7 @@ #include <akonadi/transactionsequence.h> #include <akonadi/itemmodifyjob.h> #include <akonadi/session.h> +#include <akonadi/entitytreeview.h> #include "collectionfetchscope.h" #include "collectionutils_p.h" @@ -583,6 +584,12 @@ bool EntityTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, return false; } + PasteHelperJob *pasteJob = qobject_cast<PasteHelperJob*>(job); + if (pasteJob && pasteJob->hasCustomActionId()) { + d->m_pendingDrops.insert(pasteJob->customActionId(), pasteJob); + connect(job, SIGNAL(customDropAction(QString,Akonadi::Item::List,Akonadi::Collection::List,Akonadi::Collection,Qt::DropAction)), + this, SIGNAL(customDropAction(QString,Akonadi::Item::List,Akonadi::Collection::List,Akonadi::Collection,Qt::DropAction))); + } connect(job, SIGNAL(result(KJob*)), SLOT(pasteJobDone(KJob*))); // Accpet the event so that it doesn't propagate. @@ -1264,4 +1271,30 @@ QModelIndexList EntityTreeModel::modelIndexesForItem(const QAbstractItemModel *m return proxyList; } +void EntityTreeModel::customDropActionProcessed(const QString &actionId) +{ + Q_D(EntityTreeModel); + Q_ASSERT(d->m_pendingDrops.contains(actionId)); + if (!d->m_pendingDrops.contains(actionId)) { + return; + } + + d->m_pendingDrops.value(actionId)->customDropActionProcessed(); +} + +void EntityTreeModel::customDropActionProcessed(const QString &actionId, + const Item::List &items, + const Collection::List &collections, + Qt::DropAction dropAction) +{ + Q_D(EntityTreeModel); + Q_ASSERT(d->m_pendingDrops.contains(actionId)); + if (!d->m_pendingDrops.contains(actionId)) { + return; + } + + d->m_pendingDrops.value(actionId)->customDropActionProcessed(items, collections, dropAction); +} + + #include "moc_entitytreemodel.cpp" diff --git a/akonadi/entitytreemodel.h b/akonadi/entitytreemodel.h index b6533ae6b..d002f67f6 100644 --- a/akonadi/entitytreemodel.h +++ b/akonadi/entitytreemodel.h @@ -637,6 +637,36 @@ public: */ static QModelIndexList modelIndexesForItem(const QAbstractItemModel *model, const Item &item); + + /** Call when customDropAction() is handled by listener. + * + * This overload allows passing back transformed Items and Collections as + * well as changing the final DropAction that ETM should perform. EntityTreeModel + * will continue handling the drop event as if it was a @p dropAction but + * will use @p items and @p collections provided by this method instead of + * the original ones. + * + * @param actionId ID of the handled action + * @param items Items that were dropped + * @param collections Colletions that were dropped + * @param dropAction Treat the drop as this dropAction + */ + void customDropActionProcessed(const QString &actionId, + const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + Qt::DropAction dropAction); + + /** + * Call when customDropAction() is handled by listener. + * + * This overload indicates to ETM that the drop event has been handled + * completely and will not perform any additional action for this particular + * drop event. + * + * @p actionId ID of the handled action + */ + void customDropActionProcessed(const QString &actionId); + Q_SIGNALS: /** * Signal emitted when the collection tree has been fetched for the first time. @@ -665,6 +695,25 @@ Q_SIGNALS: */ void collectionFetched(int collectionId); + /** + * Emitted when custom drop action (@see EntityTreeView::addCustomDropAction) was triggered + * + * The listener can handle the drop action completely on its own and just call + * customDropActionProcessed(const QString &actionId), or it can transform the + * @p items and @p collections as needed and pass them back via the other + * customDropActionProcessed() overload and let EntityTreeModel to handle + * + * @param actionId ID of the activated action + * @param items Dropped items (if any) + * @param collections Dropped collections (if any) + * @param destination Drop destination collection + */ + void customDropAction(const QString &actionId, + const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &destination, + Qt::DropAction dropAction); + protected: /** * Clears and resets the model. Always call this instead of the reset method in the superclass. diff --git a/akonadi/entitytreemodel_p.cpp b/akonadi/entitytreemodel_p.cpp index 07b107841..d4802dfa7 100644 --- a/akonadi/entitytreemodel_p.cpp +++ b/akonadi/entitytreemodel_p.cpp @@ -24,6 +24,7 @@ #include "dbusconnectionpool.h" #include "monitor_p.h" // For friend ref/deref #include "servermanager.h" +#include "pastehelper_p.h" #include <KDE/KLocalizedString> #include <KDE/KMessageBox> @@ -1402,6 +1403,12 @@ void EntityTreeModelPrivate::pasteJobDone(KJob *job) void EntityTreeModelPrivate::updateJobDone(KJob *job) { + PasteHelperJob *phj = qobject_cast<PasteHelperJob*>(job); + Q_ASSERT(phj); + if (phj->hasCustomActionId()) { + m_pendingDrops.remove(phj->customActionId()); + } + if (job->error()) { // TODO: handle job errors kWarning() << "Job error:" << job->errorString(); diff --git a/akonadi/entitytreemodel_p.h b/akonadi/entitytreemodel_p.h index d29ad901d..500210591 100644 --- a/akonadi/entitytreemodel_p.h +++ b/akonadi/entitytreemodel_p.h @@ -36,6 +36,7 @@ namespace Akonadi class ItemFetchJob; class ChangeRecorder; class AgentInstance; +class PasteHelperJob; } struct Node @@ -141,6 +142,7 @@ public: Node *m_rootNode; QString m_rootCollectionDisplayName; QStringList m_mimeTypeFilter; + QMap<QString, PasteHelperJob*> m_pendingDrops; MimeTypeChecker m_mimeChecker; EntityTreeModel::CollectionFetchStrategy m_collectionFetchStrategy; EntityTreeModel::ItemPopulationStrategy m_itemPopulation; diff --git a/akonadi/entitytreeview.cpp b/akonadi/entitytreeview.cpp index 9873835c4..06cc21e3b 100644 --- a/akonadi/entitytreeview.cpp +++ b/akonadi/entitytreeview.cpp @@ -340,4 +340,16 @@ void EntityTreeView::setDefaultPopupMenu(const QString &name) d->mDefaultPopupMenu = name; } + +#ifndef QT_NO_DRAGANDDROP +void EntityTreeView::addCustomDropAction(const QString &actionId, + const KIcon &icon, + const QString &text, + Qt::DropAction dropAction) +{ + d->mDragDropManager->addCustomAction(actionId, icon, text, dropAction); +} +#endif + + #include "moc_entitytreeview.cpp" diff --git a/akonadi/entitytreeview.h b/akonadi/entitytreeview.h index 7ab662ae3..e12ca8a88 100644 --- a/akonadi/entitytreeview.h +++ b/akonadi/entitytreeview.h @@ -25,9 +25,12 @@ #include "akonadi_export.h" #include <QTreeView> +#include <akonadi/collection.h> +#include <akonadi/item.h> class KXMLGUIClient; class QDragMoveEvent; +class KIcon; namespace Akonadi { @@ -167,6 +170,21 @@ public: */ void setDefaultPopupMenu(const QString &name); +#ifndef QT_NO_DRAGANDDROP + /** + * Add custom action to the popup menu that appears when drag is finished. + * + * @param actionId Custom action identifier + * @param icon Action icon + * @param text Action label + * @param dropAction Type of the action + */ + void addCustomDropAction(const QString &actionId, + const KIcon &icon, + const QString &text, + Qt::DropAction dropAction); +#endif + Q_SIGNALS: /** * This signal is emitted whenever the user has clicked @@ -216,6 +234,27 @@ Q_SIGNALS: */ void currentChanged(const Akonadi::Item &item); +#ifndef QT_NO_DRAGANDDROP + /** + * Emitted when custom drop action was triggered + * + * The listener can handle the drop action completely on its own and just call + * customDropActionProcessed(const QString &actionId), or it can transform the + * @p items and @p collections as needed and pass them back via the other + * customDropActionProcessed() overload and let EntityTreeModel to handle + * + * @param actionId ID of the activated action + * @param items Dropped items (if any) + * @param collections Dropped collections (if any) + * @param destination Drop destination collection + */ + void customDropAction(const QString &actionId, + const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &destination, + Qt::DropAction dropAction); +#endif + protected: using QTreeView::currentChanged; #ifndef QT_NO_DRAGANDDROP diff --git a/akonadi/pastehelper.cpp b/akonadi/pastehelper.cpp index e9929d597..7b92f345d 100644 --- a/akonadi/pastehelper.cpp +++ b/akonadi/pastehelper.cpp @@ -28,7 +28,6 @@ #include "itemmodifyjob.h" #include "itemmovejob.h" #include "linkjob.h" -#include "transactionsequence.h" #include "session.h" #include "unlinkjob.h" @@ -38,65 +37,78 @@ #include <QtCore/QByteArray> #include <QtCore/QMimeData> #include <QtCore/QStringList> -#include <QtCore/QMutexLocker> +#include <QtCore/QTimer> #include <boost/bind.hpp> using namespace Akonadi; -class PasteHelperJob: public Akonadi::TransactionSequence -{ - Q_OBJECT - -public: - explicit PasteHelperJob(Qt::DropAction action, const Akonadi::Item::List &items, - const Akonadi::Collection::List &collections, - const Akonadi::Collection &destination, - QObject *parent = 0); - virtual ~PasteHelperJob(); - -private Q_SLOTS: - void onDragSourceCollectionFetched(KJob *job); - -private: - void runActions(); - void runItemsActions(); - void runCollectionsActions(); - -private: - Qt::DropAction mAction; - Akonadi::Item::List mItems; - Akonadi::Collection::List mCollections; - Akonadi::Collection mDestCollection; -}; - PasteHelperJob::PasteHelperJob(Qt::DropAction action, const Item::List &items, const Collection::List &collections, const Collection &destination, + const QString &customActionId, QObject *parent) : TransactionSequence(parent) , mAction(action) , mItems(items) , mCollections(collections) , mDestCollection(destination) + , mCustomActionId(customActionId) { //FIXME: The below code disables transactions in otder to avoid data loss due to nested //transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads). //Remove once this is fixed properly, see the other FIXME comments. setProperty("transactionsDisabled", true); + if (mCustomActionId.isEmpty()) { + processDropAction(); + } else{ + setAutoDelete(false); + QTimer::singleShot(0, this, SLOT(emitCustomDropAction())); + } +} + +PasteHelperJob::~PasteHelperJob() +{ +} + +void PasteHelperJob::emitCustomDropAction() +{ + Q_EMIT customDropAction(mCustomActionId, mItems, mCollections, mDestCollection, mAction); +} + +void PasteHelperJob::customDropActionProcessed() +{ + commit(); + return; +} + +void PasteHelperJob::customDropActionProcessed(const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + Qt::DropAction dropAction) +{ + setAutoDelete(true); + mItems = items; + mCollections = collections; + mAction = dropAction; + + processDropAction(); +} + +void PasteHelperJob::processDropAction() +{ Collection dragSourceCollection; - if (!items.isEmpty() && items.first().parentCollection().isValid()) { + if (!mItems.isEmpty() && mItems.first().parentCollection().isValid()) { // Check if all items have the same parent collection ID - const Collection parent = items.first().parentCollection(); - if (std::find_if(items.constBegin(), items.constEnd(), + const Collection parent = mItems.first().parentCollection(); + if (std::find_if(mItems.constBegin(), mItems.constEnd(), boost::bind(&Entity::operator!=, boost::bind(static_cast<Collection (Item::*)() const>(&Item::parentCollection), _1), parent)) - == items.constEnd()) + == mItems.constEnd()) { dragSourceCollection = parent; } - kDebug() << items.first().parentCollection().id() << dragSourceCollection.id(); + kDebug() << mItems.first().parentCollection().id() << dragSourceCollection.id(); } if (dragSourceCollection.isValid()) { @@ -114,10 +126,6 @@ PasteHelperJob::PasteHelperJob(Qt::DropAction action, const Item::List &items, } } -PasteHelperJob::~PasteHelperJob() -{ -} - void PasteHelperJob::onDragSourceCollectionFetched(KJob *job) { CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob*>(job); @@ -332,9 +340,9 @@ KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &des // TODO: handle non Akonadi URLs? } - PasteHelperJob *job = new PasteHelperJob(action, items, collections, destination, + mimeData->property("customActionId").toString(), session); return job; diff --git a/akonadi/pastehelper_p.h b/akonadi/pastehelper_p.h index 8dcec3518..9dd564b82 100644 --- a/akonadi/pastehelper_p.h +++ b/akonadi/pastehelper_p.h @@ -21,6 +21,8 @@ #define AKONADI_PASTEHELPER_P_H #include <akonadi/collection.h> +#include <akonadi/item.h> +#include <akonadi/transactionsequence.h> #include <QtCore/QList> @@ -31,6 +33,51 @@ namespace Akonadi { class Session; +class PasteHelperJob: public Akonadi::TransactionSequence +{ + Q_OBJECT + +public: + explicit PasteHelperJob(Qt::DropAction action, const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &destination, + const QString &customActionId, + QObject *parent = 0); + virtual ~PasteHelperJob(); + + void customDropActionProcessed(); + void customDropActionProcessed(const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + Qt::DropAction dropAction); + + bool hasCustomActionId() const { return !mCustomActionId.isEmpty(); } + QString customActionId() const { return mCustomActionId; } + +Q_SIGNALS: + void customDropAction(const QString &actionId, const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &collection, + Qt::DropAction dropAction); + +private Q_SLOTS: + void emitCustomDropAction(); + void onDragSourceCollectionFetched(KJob *job); + +private: + void processDropAction(); + void runActions(); + void runItemsActions(); + void runCollectionsActions(); + +private: + Qt::DropAction mAction; + Akonadi::Item::List mItems; + Akonadi::Collection::List mCollections; + Akonadi::Collection mDestCollection; + QString mCustomActionId; +}; + + /** @internal diff --git a/akonadi/tests/actionstatemanagertest.cpp b/akonadi/tests/actionstatemanagertest.cpp index 74acdf3ba..14a15af27 100644 --- a/akonadi/tests/actionstatemanagertest.cpp +++ b/akonadi/tests/actionstatemanagertest.cpp @@ -27,7 +27,7 @@ #include "../actionstatemanager.cpp" #undef QT_NO_CLIPBOARD -#include "../pastehelper.cpp" +#include "../pastehelper_p.h" typedef QHash<Akonadi::StandardActionManager::Type, bool> StateMap; Q_DECLARE_METATYPE( StateMap ) -- 2.14.1