1/*
2 Copyright (c) 2009 Stephen Kelly <steveire@gmail.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "dragdropmanager_p.h"
21#include "specialcollectionattribute_p.h"
22#include "collectionutils_p.h"
23
24#include <QApplication>
25#include <QDropEvent>
26#include <QMenu>
27#include <QDrag>
28
29#include <KDE/KIcon>
30#include <KDE/KLocalizedString>
31#include <KDE/KUrl>
32
33#include "akonadi/collection.h"
34#include "akonadi/entitytreemodel.h"
35
36using namespace Akonadi;
37
38DragDropManager::DragDropManager(QAbstractItemView *view)
39 : mShowDropActionMenu(true)
40 , mIsManualSortingActive(false)
41 , m_view(view)
42{
43}
44
45Akonadi::Collection DragDropManager::currentDropTarget(QDropEvent *event) const
46{
47 const QModelIndex index = m_view->indexAt(event->pos());
48 Collection collection = m_view->model()->data(index, EntityTreeModel::CollectionRole).value<Collection>();
49 if (!collection.isValid()) {
50 const Item item = m_view->model()->data(index, EntityTreeModel::ItemRole).value<Item>();
51 if (item.isValid()) {
52 collection = m_view->model()->data(index.parent(), EntityTreeModel::CollectionRole).value<Collection>();
53 }
54 }
55
56 return collection;
57}
58
59bool DragDropManager::dropAllowed(QDragMoveEvent *event) const
60{
61 // Check if the collection under the cursor accepts this data type
62 const Collection targetCollection = currentDropTarget(event);
63 if (targetCollection.isValid()) {
64 const QStringList supportedContentTypes = targetCollection.contentMimeTypes();
65
66 const QMimeData *data = event->mimeData();
67 const KUrl::List urls = KUrl::List::fromMimeData(data);
68 foreach (const KUrl &url, urls) {
69 const Collection collection = Collection::fromUrl(url);
70 if (collection.isValid()) {
71 if (!supportedContentTypes.contains(Collection::mimeType()) &&
72 !supportedContentTypes.contains(Collection::virtualMimeType())) {
73 break;
74 }
75
76 // Check if we don't try to drop on one of the children
77 if (hasAncestor(m_view->indexAt(event->pos()), collection.id())) {
78 break;
79 }
80 } else { // This is an item.
81 const QString type = url.queryItems()[QString::fromLatin1("type")];
82 if (!supportedContentTypes.contains(type)) {
83 break;
84 }
85 }
86
87 return true;
88 }
89 }
90
91 return false;
92}
93
94bool DragDropManager::hasAncestor(const QModelIndex &_index, Collection::Id parentId) const
95{
96 QModelIndex index(_index);
97 while (index.isValid()) {
98 if (m_view->model()->data(index, EntityTreeModel::CollectionIdRole).toLongLong() == parentId) {
99 return true;
100 }
101
102 index = index.parent();
103 }
104
105 return false;
106}
107
108bool DragDropManager::processDropEvent(QDropEvent *event, bool &menuCanceled, bool dropOnItem)
109{
110 const Collection targetCollection = currentDropTarget(event);
111 if (!targetCollection.isValid()) {
112 return false;
113 }
114
115 if (!mIsManualSortingActive && !dropOnItem) {
116 return false;
117 }
118
119 const QStringList supportedContentTypes = targetCollection.contentMimeTypes();
120
121 const QMimeData *data = event->mimeData();
122 const KUrl::List urls = KUrl::List::fromMimeData(data);
123 foreach (const KUrl &url, urls) {
124 const Collection collection = Collection::fromUrl(url);
125 if (!collection.isValid()) {
126 if (!dropOnItem) {
127 return false;
128 }
129 }
130 }
131
132 int actionCount = 0;
133 Qt::DropAction defaultAction;
134 // TODO check if the source supports moving
135
136 bool moveAllowed, copyAllowed, linkAllowed;
137 moveAllowed = copyAllowed = linkAllowed = false;
138
139 if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) &&
140 (event->possibleActions() & Qt::MoveAction)) {
141 moveAllowed = true;
142 }
143 if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) &&
144 (event->possibleActions() & Qt::CopyAction)) {
145 copyAllowed = true;
146 }
147
148 if ((targetCollection.rights() & Collection::CanLinkItem) &&
149 (event->possibleActions() & Qt::LinkAction)) {
150 linkAllowed = true;
151 }
152
153 if (mIsManualSortingActive && !dropOnItem) {
154 moveAllowed = true;
155 copyAllowed = false;
156 linkAllowed = false;
157 }
158
159 if (!moveAllowed && !copyAllowed && !linkAllowed) {
160 kDebug() << "Cannot drop here:" << event->possibleActions() << m_view->model()->supportedDragActions() << m_view->model()->supportedDropActions();
161 return false;
162 }
163
164 // first check whether the user pressed a modifier key to select a specific action
165 if ((QApplication::keyboardModifiers() & Qt::ControlModifier) &&
166 (QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
167 if (linkAllowed) {
168 defaultAction = Qt::LinkAction;
169 actionCount = 1;
170 } else {
171 return false;
172 }
173 } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) {
174 if (copyAllowed) {
175 defaultAction = Qt::CopyAction;
176 actionCount = 1;
177 } else {
178 return false;
179 }
180 } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
181 if (moveAllowed) {
182 defaultAction = Qt::MoveAction;
183 actionCount = 1;
184 } else {
185 return false;
186 }
187 }
188
189 if (actionCount == 1) {
190 kDebug() << "Selecting drop action" << defaultAction << ", there are no other possibilities";
191 event->setDropAction(defaultAction);
192 return true;
193 }
194
195 if (!mShowDropActionMenu) {
196 if (moveAllowed) {
197 defaultAction = Qt::MoveAction;
198 } else if (copyAllowed) {
199 defaultAction = Qt::CopyAction;
200 } else if (linkAllowed) {
201 defaultAction = Qt::LinkAction;
202 } else {
203 return false;
204 }
205 event->setDropAction(defaultAction);
206 return true;
207 }
208
209 // otherwise show up a menu to allow the user to select an action
210 QMenu popup(m_view);
211 QAction *moveDropAction = 0;
212 QAction *copyDropAction = 0;
213 QAction *linkAction = 0;
214 QString sequence;
215
216 if (moveAllowed) {
217 sequence = QKeySequence(Qt::ShiftModifier).toString();
218 sequence.chop(1); // chop superfluous '+'
219 moveDropAction = popup.addAction(KIcon(QString::fromLatin1("go-jump")), i18n("&Move Here") + QLatin1Char('\t') + sequence);
220 }
221
222 if (copyAllowed) {
223 sequence = QKeySequence(Qt::ControlModifier).toString();
224 sequence.chop(1); // chop superfluous '+'
225 copyDropAction = popup.addAction(KIcon(QString::fromLatin1("edit-copy")), i18n("&Copy Here") + QLatin1Char('\t') + sequence);
226 }
227
228 if (linkAllowed) {
229 sequence = QKeySequence(Qt::ControlModifier + Qt::ShiftModifier).toString();
230 sequence.chop(1); // chop superfluous '+'
231 linkAction = popup.addAction(KIcon(QLatin1String("edit-link")), i18n("&Link Here") + QLatin1Char('\t') + sequence);
232 }
233
234 popup.addSeparator();
235 popup.addAction(KIcon(QString::fromLatin1("process-stop")), i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString());
236
237 QAction *activatedAction = popup.exec(QCursor::pos());
238 if (!activatedAction) {
239 menuCanceled = true;
240 return false;
241 } else if (activatedAction == moveDropAction) {
242 event->setDropAction(Qt::MoveAction);
243 } else if (activatedAction == copyDropAction) {
244 event->setDropAction(Qt::CopyAction);
245 } else if (activatedAction == linkAction) {
246 event->setDropAction(Qt::LinkAction);
247 } else {
248 menuCanceled = true;
249 return false;
250 }
251 return true;
252}
253
254void DragDropManager::startDrag(Qt::DropActions supportedActions)
255{
256 QModelIndexList indexes;
257 bool sourceDeletable = true;
258 foreach (const QModelIndex &index, m_view->selectionModel()->selectedRows()) {
259 if (!m_view->model()->flags(index).testFlag(Qt::ItemIsDragEnabled)) {
260 continue;
261 }
262
263 if (sourceDeletable) {
264 Collection source = index.data(EntityTreeModel::CollectionRole).value<Collection>();
265 if (!source.isValid()) {
266 // index points to an item
267 source = index.data(EntityTreeModel::ParentCollectionRole).value<Collection>();
268 sourceDeletable = source.rights() & Collection::CanDeleteItem;
269 } else {
270 // index points to a collection
271 sourceDeletable = (source.rights() & Collection::CanDeleteCollection) && !source.hasAttribute<SpecialCollectionAttribute>() && !source.isVirtual();
272 }
273 }
274 indexes.append(index);
275 }
276
277 if (indexes.isEmpty()) {
278 return;
279 }
280
281 QMimeData *mimeData = m_view->model()->mimeData(indexes);
282 if (!mimeData) {
283 return;
284 }
285
286 QDrag *drag = new QDrag(m_view);
287 drag->setMimeData(mimeData);
288 if (indexes.size() > 1) {
289 drag->setPixmap(KIcon(QLatin1String("document-multiple")).pixmap(QSize(22, 22)));
290 } else {
291 QPixmap pixmap = indexes.first().data(Qt::DecorationRole).value<QIcon>().pixmap(QSize(22, 22));
292 if (pixmap.isNull()) {
293 pixmap = KIcon(QLatin1String("text-plain")).pixmap(QSize(22, 22));
294 }
295 drag->setPixmap(pixmap);
296 }
297
298 if (!sourceDeletable) {
299 supportedActions &= ~Qt::MoveAction;
300 }
301
302 Qt::DropAction defaultAction = Qt::IgnoreAction;
303 if ((QApplication::keyboardModifiers() & Qt::ControlModifier) &&
304 (QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
305 defaultAction = Qt::LinkAction;
306 } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) {
307 defaultAction = Qt::CopyAction;
308 } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) {
309 defaultAction = Qt::MoveAction;
310 }
311
312 drag->exec(supportedActions, defaultAction);
313}
314
315bool DragDropManager::showDropActionMenu() const
316{
317 return mShowDropActionMenu;
318}
319
320void DragDropManager::setShowDropActionMenu(bool show)
321{
322 mShowDropActionMenu = show;
323}
324
325bool DragDropManager::isManualSortingActive() const
326{
327 return mIsManualSortingActive;
328}
329
330void DragDropManager::setManualSortingActive(bool active)
331{
332 mIsManualSortingActive = active;
333}
334