1 | /* |
2 | Copyright (c) 2008 Volker Krause <vkrause@kde.org> |
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 "pastehelper_p.h" |
21 | |
22 | #include "collectioncopyjob.h" |
23 | #include "collectionmovejob.h" |
24 | #include "collectionfetchjob.h" |
25 | #include "item.h" |
26 | #include "itemcreatejob.h" |
27 | #include "itemcopyjob.h" |
28 | #include "itemmodifyjob.h" |
29 | #include "itemmovejob.h" |
30 | #include "linkjob.h" |
31 | #include "transactionsequence.h" |
32 | #include "session.h" |
33 | #include "unlinkjob.h" |
34 | |
35 | #include <KDebug> |
36 | #include <KUrl> |
37 | |
38 | #include <QtCore/QByteArray> |
39 | #include <QtCore/QMimeData> |
40 | #include <QtCore/QStringList> |
41 | #include <QtCore/QMutexLocker> |
42 | |
43 | #include <boost/bind.hpp> |
44 | |
45 | using namespace Akonadi; |
46 | |
47 | class PasteHelperJob: public Akonadi::TransactionSequence |
48 | { |
49 | Q_OBJECT |
50 | |
51 | public: |
52 | explicit PasteHelperJob(Qt::DropAction action, const Akonadi::Item::List &items, |
53 | const Akonadi::Collection::List &collections, |
54 | const Akonadi::Collection &destination, |
55 | QObject *parent = 0); |
56 | virtual ~PasteHelperJob(); |
57 | |
58 | private Q_SLOTS: |
59 | void onDragSourceCollectionFetched(KJob *job); |
60 | |
61 | private: |
62 | void runActions(); |
63 | void runItemsActions(); |
64 | void runCollectionsActions(); |
65 | |
66 | private: |
67 | Qt::DropAction mAction; |
68 | Akonadi::Item::List mItems; |
69 | Akonadi::Collection::List mCollections; |
70 | Akonadi::Collection mDestCollection; |
71 | }; |
72 | |
73 | PasteHelperJob::PasteHelperJob(Qt::DropAction action, const Item::List &items, |
74 | const Collection::List &collections, |
75 | const Collection &destination, |
76 | QObject *parent) |
77 | : TransactionSequence(parent) |
78 | , mAction(action) |
79 | , mItems(items) |
80 | , mCollections(collections) |
81 | , mDestCollection(destination) |
82 | { |
83 | //FIXME: The below code disables transactions in otder to avoid data loss due to nested |
84 | //transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads). |
85 | //Remove once this is fixed properly, see the other FIXME comments. |
86 | setProperty("transactionsDisabled" , true); |
87 | |
88 | Collection dragSourceCollection; |
89 | if (!items.isEmpty() && items.first().parentCollection().isValid()) { |
90 | // Check if all items have the same parent collection ID |
91 | const Collection parent = items.first().parentCollection(); |
92 | if (std::find_if(items.constBegin(), items.constEnd(), |
93 | boost::bind(&Entity::operator!=, boost::bind(static_cast<Collection (Item::*)() const>(&Item::parentCollection), _1), parent)) |
94 | == items.constEnd()) |
95 | { |
96 | dragSourceCollection = parent; |
97 | } |
98 | } |
99 | |
100 | kDebug() << items.first().parentCollection().id() << dragSourceCollection.id(); |
101 | |
102 | if (dragSourceCollection.isValid()) { |
103 | // Disable autocommitting, because starting a Link/Unlink/Copy/Move job |
104 | // after the transaction has ended leaves the job hanging |
105 | setAutomaticCommittingEnabled(false); |
106 | |
107 | CollectionFetchJob *fetch = new CollectionFetchJob(dragSourceCollection, |
108 | CollectionFetchJob::Base, |
109 | this); |
110 | QObject::connect(fetch, SIGNAL(finished(KJob*)), |
111 | this, SLOT(onDragSourceCollectionFetched(KJob*))); |
112 | } else { |
113 | runActions(); |
114 | } |
115 | } |
116 | |
117 | PasteHelperJob::~PasteHelperJob() |
118 | { |
119 | } |
120 | |
121 | void PasteHelperJob::onDragSourceCollectionFetched(KJob *job) |
122 | { |
123 | CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob*>(job); |
124 | kDebug() << fetch->error() << fetch->collections().count(); |
125 | if (fetch->error() || fetch->collections().count() != 1) { |
126 | runActions(); |
127 | commit(); |
128 | return; |
129 | } |
130 | |
131 | |
132 | // If the source collection is virtual, treat copy and move actions differently |
133 | const Collection sourceCollection = fetch->collections().first(); |
134 | kDebug() << "FROM: " << sourceCollection.id() << sourceCollection.name() << sourceCollection.isVirtual(); |
135 | kDebug() << "DEST: " << mDestCollection.id() << mDestCollection.name() << mDestCollection.isVirtual(); |
136 | kDebug() << "ACTN:" << mAction; |
137 | if (sourceCollection.isVirtual()) { |
138 | switch (mAction) { |
139 | case Qt::CopyAction: |
140 | if (mDestCollection.isVirtual()) { |
141 | new LinkJob(mDestCollection, mItems, this); |
142 | } else { |
143 | new ItemCopyJob(mItems, mDestCollection, this); |
144 | } |
145 | break; |
146 | case Qt::MoveAction: |
147 | new UnlinkJob(sourceCollection, mItems, this); |
148 | if (mDestCollection.isVirtual()) { |
149 | new LinkJob(mDestCollection, mItems, this); |
150 | } else { |
151 | new ItemCopyJob(mItems, mDestCollection, this); |
152 | } |
153 | break; |
154 | case Qt::LinkAction: |
155 | new LinkJob(mDestCollection, mItems, this); |
156 | break; |
157 | default: |
158 | Q_ASSERT(false); |
159 | } |
160 | runCollectionsActions(); |
161 | commit(); |
162 | } else { |
163 | runActions(); |
164 | } |
165 | |
166 | commit(); |
167 | } |
168 | |
169 | void PasteHelperJob::runActions() |
170 | { |
171 | runItemsActions(); |
172 | runCollectionsActions(); |
173 | } |
174 | |
175 | void PasteHelperJob::runItemsActions() |
176 | { |
177 | if (mItems.isEmpty()) { |
178 | return; |
179 | } |
180 | |
181 | switch (mAction) { |
182 | case Qt::CopyAction: |
183 | new ItemCopyJob(mItems, mDestCollection, this); |
184 | break; |
185 | case Qt::MoveAction: |
186 | new ItemMoveJob(mItems, mDestCollection, this); |
187 | break; |
188 | case Qt::LinkAction: |
189 | new LinkJob(mDestCollection, mItems, this); |
190 | break; |
191 | default: |
192 | Q_ASSERT(false); // WTF?! |
193 | } |
194 | } |
195 | |
196 | void PasteHelperJob::runCollectionsActions() |
197 | { |
198 | if (mCollections.isEmpty()) { |
199 | return; |
200 | } |
201 | |
202 | switch (mAction) { |
203 | case Qt::CopyAction: |
204 | foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well |
205 | new CollectionCopyJob(col, mDestCollection, this); |
206 | } |
207 | break; |
208 | case Qt::MoveAction: |
209 | foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well |
210 | new CollectionMoveJob(col, mDestCollection, this); |
211 | } |
212 | break; |
213 | case Qt::LinkAction: |
214 | // Not supported for collections |
215 | break; |
216 | default: |
217 | Q_ASSERT(false); // WTF?! |
218 | } |
219 | } |
220 | |
221 | |
222 | |
223 | bool PasteHelper::canPaste(const QMimeData *mimeData, const Collection &collection) |
224 | { |
225 | if (!mimeData || !collection.isValid()) { |
226 | return false; |
227 | } |
228 | |
229 | // check that the target collection has the rights to |
230 | // create the pasted items resp. collections |
231 | Collection::Rights neededRights = Collection::ReadOnly; |
232 | if (KUrl::List::canDecode(mimeData)) { |
233 | const KUrl::List urls = KUrl::List::fromMimeData(mimeData); |
234 | foreach (const KUrl &url, urls) { |
235 | if (url.hasQueryItem(QLatin1String("item" ))) { |
236 | neededRights |= Collection::CanCreateItem; |
237 | } else if (url.hasQueryItem(QLatin1String("collection" ))) { |
238 | neededRights |= Collection::CanCreateCollection; |
239 | } |
240 | } |
241 | |
242 | if ((collection.rights() & neededRights) == 0) { |
243 | return false; |
244 | } |
245 | |
246 | // check that the target collection supports the mime types of the |
247 | // items/collections that shall be pasted |
248 | bool supportsMimeTypes = true; |
249 | foreach (const KUrl &url, urls) { |
250 | // collections do not provide mimetype information, so ignore this check |
251 | if (url.hasQueryItem(QLatin1String("collection" ))) { |
252 | continue; |
253 | } |
254 | |
255 | const QString mimeType = url.queryItemValue(QLatin1String("type" )); |
256 | if (!collection.contentMimeTypes().contains(mimeType)) { |
257 | supportsMimeTypes = false; |
258 | break; |
259 | } |
260 | } |
261 | |
262 | if (!supportsMimeTypes) { |
263 | return false; |
264 | } |
265 | |
266 | return true; |
267 | } |
268 | |
269 | return false; |
270 | } |
271 | |
272 | KJob *PasteHelper::paste(const QMimeData *mimeData, const Collection &collection, bool copy, Session *session) |
273 | { |
274 | if (!canPaste(mimeData, collection)) { |
275 | return 0; |
276 | } |
277 | |
278 | // we try to drop data not coming with the akonadi:// url |
279 | // find a type the target collection supports |
280 | foreach (const QString &type, mimeData->formats()) { |
281 | if (!collection.contentMimeTypes().contains(type)) { |
282 | continue; |
283 | } |
284 | |
285 | QByteArray item = mimeData->data(type); |
286 | // HACK for some unknown reason the data is sometimes 0-terminated... |
287 | if (!item.isEmpty() && item.at(item.size() - 1) == 0) { |
288 | item.resize(item.size() - 1); |
289 | } |
290 | |
291 | Item it; |
292 | it.setMimeType(type); |
293 | it.setPayloadFromData(item); |
294 | |
295 | ItemCreateJob *job = new ItemCreateJob(it, collection); |
296 | return job; |
297 | } |
298 | |
299 | if (!KUrl::List::canDecode(mimeData)) { |
300 | return 0; |
301 | } |
302 | |
303 | // data contains an url list |
304 | return pasteUriList(mimeData, collection, copy ? Qt::CopyAction : Qt::MoveAction, session); |
305 | } |
306 | |
307 | KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &destination, Qt::DropAction action, Session *session) |
308 | { |
309 | if (!KUrl::List::canDecode(mimeData)) { |
310 | return 0; |
311 | } |
312 | |
313 | if (!canPaste(mimeData, destination)) { |
314 | return 0; |
315 | } |
316 | |
317 | const KUrl::List urls = KUrl::List::fromMimeData(mimeData); |
318 | Collection::List collections; |
319 | Item::List items; |
320 | foreach (const KUrl &url, urls) { |
321 | const Collection collection = Collection::fromUrl(url); |
322 | if (collection.isValid()) { |
323 | collections.append(collection); |
324 | } |
325 | Item item = Item::fromUrl(url); |
326 | if (url.hasQueryItem(QLatin1String("parent" ))) { |
327 | item.setParentCollection(Collection(url.queryItem(QLatin1String("parent" )).toLongLong())); |
328 | } |
329 | if (item.isValid()) { |
330 | items.append(item); |
331 | } |
332 | // TODO: handle non Akonadi URLs? |
333 | } |
334 | |
335 | |
336 | PasteHelperJob *job = new PasteHelperJob(action, items, |
337 | collections, destination, |
338 | session); |
339 | |
340 | return job; |
341 | } |
342 | |
343 | #include "pastehelper.moc" |
344 | |