File indexing completed on 2024-06-16 04:50:37

0001 /*
0002     SPDX-FileCopyrightText: 2009 Stephen Kelly <steveire@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "akonadiwidgets_debug.h"
0008 #include "collectionutils.h"
0009 #include "dragdropmanager_p.h"
0010 #include "specialcollectionattribute.h"
0011 #include <QApplication>
0012 #include <QDrag>
0013 #include <QDropEvent>
0014 #include <QMenu>
0015 
0016 #include <KLocalizedString>
0017 #include <QMimeData>
0018 #include <QUrl>
0019 #include <QUrlQuery>
0020 
0021 #include "collection.h"
0022 #include "entitytreemodel.h"
0023 
0024 using namespace Akonadi;
0025 
0026 DragDropManager::DragDropManager(QAbstractItemView *view)
0027     : m_view(view)
0028 {
0029 }
0030 
0031 Akonadi::Collection DragDropManager::currentDropTarget(QDropEvent *event) const
0032 {
0033     const QModelIndex index = m_view->indexAt(event->position().toPoint());
0034     auto collection = m_view->model()->data(index, EntityTreeModel::CollectionRole).value<Collection>();
0035     if (!collection.isValid()) {
0036         const Item item = m_view->model()->data(index, EntityTreeModel::ItemRole).value<Item>();
0037         if (item.isValid()) {
0038             collection = m_view->model()->data(index.parent(), EntityTreeModel::CollectionRole).value<Collection>();
0039         }
0040     }
0041 
0042     return collection;
0043 }
0044 
0045 bool DragDropManager::dropAllowed(QDragMoveEvent *event) const
0046 {
0047     // Check if the collection under the cursor accepts this data type
0048     const Collection targetCollection = currentDropTarget(event);
0049     if (targetCollection.isValid()) {
0050         const QStringList supportedContentTypes = targetCollection.contentMimeTypes();
0051 
0052         const QMimeData *data = event->mimeData();
0053         if (!data) {
0054             return false;
0055         }
0056         const QList<QUrl> urls = data->urls();
0057         for (const QUrl &url : urls) {
0058             const Collection collection = Collection::fromUrl(url);
0059             if (collection.isValid()) {
0060                 if (!supportedContentTypes.contains(Collection::mimeType()) && !supportedContentTypes.contains(Collection::virtualMimeType())) {
0061                     break;
0062                 }
0063 
0064                 // Check if we don't try to drop on one of the children
0065                 if (hasAncestor(m_view->indexAt(event->position().toPoint()), collection.id())) {
0066                     break;
0067                 }
0068             } else { // This is an item.
0069                 const QList<QPair<QString, QString>> query = QUrlQuery(url).queryItems();
0070                 for (int i = 0; i < query.count(); ++i) {
0071                     if (query.at(i).first == QLatin1StringView("type")) {
0072                         const QString type = query.at(i).second;
0073                         if (!supportedContentTypes.contains(type)) {
0074                             break;
0075                         }
0076                     }
0077                 }
0078             }
0079             return true;
0080         }
0081     }
0082 
0083     return false;
0084 }
0085 
0086 bool DragDropManager::hasAncestor(const QModelIndex &_index, Collection::Id parentId) const
0087 {
0088     QModelIndex index(_index);
0089     while (index.isValid()) {
0090         if (m_view->model()->data(index, EntityTreeModel::CollectionIdRole).toLongLong() == parentId) {
0091             return true;
0092         }
0093 
0094         index = index.parent();
0095     }
0096 
0097     return false;
0098 }
0099 
0100 bool DragDropManager::processDropEvent(QDropEvent *event, bool &menuCanceled, bool dropOnItem)
0101 {
0102     const Collection targetCollection = currentDropTarget(event);
0103     if (!targetCollection.isValid()) {
0104         return false;
0105     }
0106 
0107     if (!mIsManualSortingActive && !dropOnItem) {
0108         return false;
0109     }
0110 
0111     const QMimeData *data = event->mimeData();
0112     if (!data) {
0113         return false;
0114     }
0115     const QList<QUrl> urls = data->urls();
0116     for (const QUrl &url : urls) {
0117         const Collection collection = Collection::fromUrl(url);
0118         if (!collection.isValid()) {
0119             if (!dropOnItem) {
0120                 return false;
0121             }
0122         }
0123     }
0124 
0125     int actionCount = 0;
0126     Qt::DropAction defaultAction;
0127     // TODO check if the source supports moving
0128 
0129     bool moveAllowed;
0130     bool copyAllowed;
0131     bool linkAllowed;
0132     moveAllowed = copyAllowed = linkAllowed = false;
0133 
0134     if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) && (event->possibleActions() & Qt::MoveAction)) {
0135         moveAllowed = true;
0136     }
0137     if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) && (event->possibleActions() & Qt::CopyAction)) {
0138         copyAllowed = true;
0139     }
0140 
0141     if ((targetCollection.rights() & Collection::CanLinkItem) && (event->possibleActions() & Qt::LinkAction)) {
0142         linkAllowed = true;
0143     }
0144 
0145     if (mIsManualSortingActive && !dropOnItem) {
0146         moveAllowed = true;
0147         copyAllowed = false;
0148         linkAllowed = false;
0149     }
0150 
0151     if (!moveAllowed && !copyAllowed && !linkAllowed) {
0152         qCDebug(AKONADIWIDGETS_LOG) << "Cannot drop here:" << event->possibleActions() << m_view->model()->supportedDragActions()
0153                                     << m_view->model()->supportedDropActions();
0154         return false;
0155     }
0156 
0157     // first check whether the user pressed a modifier key to select a specific action
0158     if ((QApplication::keyboardModifiers() & Qt::ControlModifier) && (QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
0159         if (linkAllowed) {
0160             defaultAction = Qt::LinkAction;
0161             actionCount = 1;
0162         } else {
0163             return false;
0164         }
0165     } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) {
0166         if (copyAllowed) {
0167             defaultAction = Qt::CopyAction;
0168             actionCount = 1;
0169         } else {
0170             return false;
0171         }
0172     } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
0173         if (moveAllowed) {
0174             defaultAction = Qt::MoveAction;
0175             actionCount = 1;
0176         } else {
0177             return false;
0178         }
0179     }
0180 
0181     if (actionCount == 1) {
0182         qCDebug(AKONADIWIDGETS_LOG) << "Selecting drop action" << defaultAction << ", there are no other possibilities";
0183         event->setDropAction(defaultAction);
0184         return true;
0185     }
0186 
0187     if (!mShowDropActionMenu) {
0188         if (moveAllowed) {
0189             defaultAction = Qt::MoveAction;
0190         } else if (copyAllowed) {
0191             defaultAction = Qt::CopyAction;
0192         } else if (linkAllowed) {
0193             defaultAction = Qt::LinkAction;
0194         } else {
0195             return false;
0196         }
0197         event->setDropAction(defaultAction);
0198         return true;
0199     }
0200 
0201     // otherwise show up a menu to allow the user to select an action
0202     QMenu popup(m_view);
0203     QAction *moveDropAction = nullptr;
0204     QAction *copyDropAction = nullptr;
0205     QAction *linkAction = nullptr;
0206     QString sequence;
0207 
0208     if (moveAllowed) {
0209         sequence = QKeySequence(Qt::ShiftModifier).toString();
0210         sequence.chop(1); // chop superfluous '+'
0211         moveDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-move"), QIcon::fromTheme(QStringLiteral("go-jump"))),
0212                                          i18n("&Move Here") + QLatin1Char('\t') + sequence);
0213     }
0214 
0215     if (copyAllowed) {
0216         sequence = QKeySequence(Qt::ControlModifier).toString();
0217         sequence.chop(1); // chop superfluous '+'
0218         copyDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy Here") + QLatin1Char('\t') + sequence);
0219     }
0220 
0221     if (linkAllowed) {
0222         sequence = QKeySequence(Qt::ControlModifier | Qt::ShiftModifier).toString();
0223         sequence.chop(1); // chop superfluous '+'
0224         linkAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-link")), i18n("&Link Here") + QLatin1Char('\t') + sequence);
0225     }
0226 
0227     popup.addSeparator();
0228     QAction *cancelAction =
0229         popup.addAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString());
0230 
0231     QAction *activatedAction = popup.exec(m_view->viewport()->mapToGlobal(event->position().toPoint()));
0232     if (!activatedAction || (activatedAction == cancelAction)) {
0233         menuCanceled = true;
0234         return false;
0235     } else if (activatedAction == moveDropAction) {
0236         event->setDropAction(Qt::MoveAction);
0237     } else if (activatedAction == copyDropAction) {
0238         event->setDropAction(Qt::CopyAction);
0239     } else if (activatedAction == linkAction) {
0240         event->setDropAction(Qt::LinkAction);
0241     }
0242     return true;
0243 }
0244 
0245 void DragDropManager::startDrag(Qt::DropActions supportedActions)
0246 {
0247     QModelIndexList indexes;
0248     bool sourceDeletable = true;
0249     const QModelIndexList lstModel = m_view->selectionModel()->selectedRows();
0250     for (const QModelIndex &index : lstModel) {
0251         if (!m_view->model()->flags(index).testFlag(Qt::ItemIsDragEnabled)) {
0252             continue;
0253         }
0254 
0255         if (sourceDeletable) {
0256             auto source = index.data(EntityTreeModel::CollectionRole).value<Collection>();
0257             if (!source.isValid()) {
0258                 // index points to an item
0259                 source = index.data(EntityTreeModel::ParentCollectionRole).value<Collection>();
0260                 sourceDeletable = source.rights() & Collection::CanDeleteItem;
0261             } else {
0262                 // index points to a collection
0263                 sourceDeletable =
0264                     (source.rights() & Collection::CanDeleteCollection) && !source.hasAttribute<SpecialCollectionAttribute>() && !source.isVirtual();
0265             }
0266         }
0267         indexes.append(index);
0268     }
0269 
0270     if (indexes.isEmpty()) {
0271         return;
0272     }
0273 
0274     QMimeData *mimeData = m_view->model()->mimeData(indexes);
0275     if (!mimeData) {
0276         return;
0277     }
0278 
0279     auto drag = new QDrag(m_view);
0280     drag->setMimeData(mimeData);
0281     if (indexes.size() > 1) {
0282         drag->setPixmap(QIcon::fromTheme(QStringLiteral("document-multiple")).pixmap(QSize(22, 22)));
0283     } else {
0284         QPixmap pixmap = indexes.first().data(Qt::DecorationRole).value<QIcon>().pixmap(QSize(22, 22));
0285         if (pixmap.isNull()) {
0286             pixmap = QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(QSize(22, 22));
0287         }
0288         drag->setPixmap(pixmap);
0289     }
0290 
0291     if (!sourceDeletable) {
0292         supportedActions &= ~Qt::MoveAction;
0293     }
0294 
0295     Qt::DropAction defaultAction = Qt::IgnoreAction;
0296     if ((QApplication::keyboardModifiers() & Qt::ControlModifier) && (QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
0297         defaultAction = Qt::LinkAction;
0298     } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) {
0299         defaultAction = Qt::CopyAction;
0300     } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
0301         defaultAction = Qt::MoveAction;
0302     }
0303 
0304     drag->exec(supportedActions, defaultAction);
0305 }
0306 
0307 bool DragDropManager::showDropActionMenu() const
0308 {
0309     return mShowDropActionMenu;
0310 }
0311 
0312 void DragDropManager::setShowDropActionMenu(bool show)
0313 {
0314     mShowDropActionMenu = show;
0315 }
0316 
0317 bool DragDropManager::isManualSortingActive() const
0318 {
0319     return mIsManualSortingActive;
0320 }
0321 
0322 void DragDropManager::setManualSortingActive(bool active)
0323 {
0324     mIsManualSortingActive = active;
0325 }