1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "dropjob.h"
9
10#include "job_p.h"
11#include "jobuidelegate.h"
12#include "jobuidelegateextension.h"
13#include "kio_widgets_debug.h"
14#include "pastejob.h"
15#include "pastejob_p.h"
16
17#include <KConfigGroup>
18#include <KCoreDirLister>
19#include <KDesktopFile>
20#include <KFileItem>
21#include <KFileItemListProperties>
22#include <KIO/ApplicationLauncherJob>
23#include <KIO/CopyJob>
24#include <KIO/DndPopupMenuPlugin>
25#include <KIO/FileUndoManager>
26#include <KJobWidgets>
27#include <KJobWindows>
28#include <KLocalizedString>
29#include <KMountPoint>
30#include <KPluginFactory>
31#include <KPluginMetaData>
32#include <KProtocolManager>
33#include <KService>
34#include <KSharedConfig>
35#include <KUrlMimeData>
36
37#ifdef WITH_QTDBUS
38#include <QDBusConnection>
39#include <QDBusPendingCall>
40#endif
41
42#include <QDropEvent>
43#include <QFileInfo>
44#include <QMenu>
45#include <QMetaEnum>
46#include <QMimeData>
47#include <QProcess>
48#include <QTimer>
49#include <QWindow>
50
51using namespace KIO;
52
53Q_DECLARE_METATYPE(Qt::DropAction)
54
55namespace KIO
56{
57class DropMenu;
58}
59
60class KIO::DropMenu : public QMenu
61{
62 Q_OBJECT
63public:
64 explicit DropMenu(QWidget *parent = nullptr);
65 ~DropMenu() override;
66
67 void addCancelAction();
68 void addExtraActions(const QList<QAction *> &appActions, const QList<QAction *> &pluginActions);
69
70private:
71 QList<QAction *> m_appActions;
72 QList<QAction *> m_pluginActions;
73 QAction *m_lastSeparator;
74 QAction *m_extraActionsSeparator;
75 QAction *m_cancelAction;
76};
77
78static const QString s_applicationSlashXDashKDEDashArkDashDnDExtractDashService = //
79 QStringLiteral("application/x-kde-ark-dndextract-service");
80static const QString s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath = //
81 QStringLiteral("application/x-kde-ark-dndextract-path");
82
83class KIO::DropJobPrivate : public KIO::JobPrivate
84{
85public:
86 DropJobPrivate(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
87 : JobPrivate()
88 , m_mimeData(dropEvent->mimeData()) // Extract everything from the dropevent, since it will be deleted before the job starts
89 , m_urls(KUrlMimeData::urlsFromMimeData(mimeData: m_mimeData, decodeOptions: KUrlMimeData::PreferLocalUrls, metaData: &m_metaData))
90 , m_dropAction(dropEvent->dropAction())
91 , m_possibleActions(dropEvent->possibleActions())
92 , m_relativePos(dropEvent->position().toPoint())
93 , m_keyboardModifiers(dropEvent->modifiers())
94 , m_hasArkFormat(m_mimeData->hasFormat(mimetype: s_applicationSlashXDashKDEDashArkDashDnDExtractDashService)
95 && m_mimeData->hasFormat(mimetype: s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath))
96 , m_destUrl(destUrl)
97 , m_destItem(KCoreDirLister::cachedItemForUrl(url: destUrl))
98 , m_flags(flags)
99 , m_dropjobFlags(dropjobFlags)
100 , m_triggered(false)
101 {
102 // Check for the drop of a bookmark -> we want a Link action
103 if (m_mimeData->hasFormat(QStringLiteral("application/x-xbel"))) {
104 m_keyboardModifiers |= Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier);
105 m_dropAction = Qt::LinkAction;
106 }
107 if (m_destItem.isNull() && m_destUrl.isLocalFile()) {
108 m_destItem = KFileItem(m_destUrl);
109 }
110
111 if (m_hasArkFormat) {
112 m_remoteArkDBusClient = QString::fromUtf8(ba: m_mimeData->data(mimetype: s_applicationSlashXDashKDEDashArkDashDnDExtractDashService));
113 m_remoteArkDBusPath = QString::fromUtf8(ba: m_mimeData->data(mimetype: s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath));
114 }
115
116 if (!(m_flags & KIO::NoPrivilegeExecution)) {
117 m_privilegeExecutionEnabled = true;
118 switch (m_dropAction) {
119 case Qt::CopyAction:
120 m_operationType = Copy;
121 break;
122 case Qt::MoveAction:
123 m_operationType = Move;
124 break;
125 case Qt::LinkAction:
126 m_operationType = Symlink;
127 break;
128 default:
129 m_operationType = Other;
130 break;
131 }
132 }
133 }
134
135 bool destIsDirectory() const
136 {
137 if (!m_destItem.isNull()) {
138 return m_destItem.isDir();
139 }
140 // We support local dir, remote dir, local desktop file, local executable.
141 // So for remote URLs, we just assume they point to a directory, the user will get an error from KIO::copy if not.
142 return true;
143 }
144 void handleCopyToDirectory();
145 void slotDropActionDetermined(int error);
146 void handleDropToDesktopFile();
147 void handleDropToExecutable();
148 void fillPopupMenu(KIO::DropMenu *popup);
149 void addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps);
150 void doCopyToDirectory();
151
152 QWindow *transientParent();
153
154 QPointer<const QMimeData> m_mimeData;
155 const QList<QUrl> m_urls;
156 QMap<QString, QString> m_metaData;
157 Qt::DropAction m_dropAction;
158 Qt::DropActions m_possibleActions;
159 bool m_allSourcesAreHttpUrls;
160 QPoint m_relativePos;
161 Qt::KeyboardModifiers m_keyboardModifiers;
162 KFileItemListProperties m_itemProps;
163 bool m_hasArkFormat;
164 QString m_remoteArkDBusClient;
165 QString m_remoteArkDBusPath;
166 QUrl m_destUrl;
167 KFileItem m_destItem; // null for remote URLs not found in the dirlister cache
168 const JobFlags m_flags;
169 const DropJobFlags m_dropjobFlags;
170 QList<QAction *> m_appActions;
171 QList<QAction *> m_pluginActions;
172 bool m_triggered; // Tracks whether an action has been triggered in the popup menu.
173 QSet<KIO::DropMenu *> m_menus;
174
175 Q_DECLARE_PUBLIC(DropJob)
176
177 void slotStart();
178 void slotTriggered(QAction *);
179 void slotAboutToHide();
180
181 static inline DropJob *newJob(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
182 {
183 DropJob *job = new DropJob(*new DropJobPrivate(dropEvent, destUrl, dropjobFlags, flags));
184 job->setUiDelegate(KIO::createDefaultJobUiDelegate());
185 // Note: never KIO::getJobTracker()->registerJob here.
186 // We don't want a progress dialog during the copy/move/link popup, it would in fact close
187 // the popup
188 return job;
189 }
190};
191
192DropMenu::DropMenu(QWidget *parent)
193 : QMenu(parent)
194 , m_extraActionsSeparator(nullptr)
195{
196 m_cancelAction = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(format: QKeySequence::NativeText), this);
197 m_cancelAction->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
198
199 m_lastSeparator = new QAction(this);
200 m_lastSeparator->setSeparator(true);
201}
202
203DropMenu::~DropMenu()
204{
205}
206
207void DropMenu::addExtraActions(const QList<QAction *> &appActions, const QList<QAction *> &pluginActions)
208{
209 removeAction(action: m_lastSeparator);
210 removeAction(action: m_cancelAction);
211
212 removeAction(action: m_extraActionsSeparator);
213 for (QAction *action : std::as_const(t&: m_appActions)) {
214 removeAction(action);
215 }
216 for (QAction *action : std::as_const(t&: m_pluginActions)) {
217 removeAction(action);
218 }
219
220 m_appActions = appActions;
221 m_pluginActions = pluginActions;
222
223 if (!m_appActions.isEmpty() || !m_pluginActions.isEmpty()) {
224 QAction *firstExtraAction = m_appActions.value(i: 0, defaultValue: m_pluginActions.value(i: 0, defaultValue: nullptr));
225 if (firstExtraAction && !firstExtraAction->isSeparator()) {
226 if (!m_extraActionsSeparator) {
227 m_extraActionsSeparator = new QAction(this);
228 m_extraActionsSeparator->setSeparator(true);
229 }
230 addAction(action: m_extraActionsSeparator);
231 }
232 addActions(actions: appActions);
233 addActions(actions: pluginActions);
234 }
235
236 addAction(action: m_lastSeparator);
237 addAction(action: m_cancelAction);
238}
239
240DropJob::DropJob(DropJobPrivate &dd)
241 : Job(dd)
242{
243 Q_D(DropJob);
244
245 QTimer::singleShot(interval: 0, receiver: this, slot: [d]() {
246 d->slotStart();
247 });
248}
249
250DropJob::~DropJob()
251{
252}
253
254void DropJobPrivate::slotStart()
255{
256 Q_Q(DropJob);
257
258#ifdef WITH_QTDBUS
259 if (m_hasArkFormat) {
260 QDBusMessage message = QDBusMessage::createMethodCall(destination: m_remoteArkDBusClient,
261 path: m_remoteArkDBusPath,
262 QStringLiteral("org.kde.ark.DndExtract"),
263 QStringLiteral("extractSelectedFilesTo"));
264 message.setArguments({m_destUrl.toDisplayString(options: QUrl::PreferLocalFile)});
265 const auto pending = QDBusConnection::sessionBus().asyncCall(message);
266 auto watcher = std::make_shared<QDBusPendingCallWatcher>(args: pending);
267 QObject::connect(sender: watcher.get(), signal: &QDBusPendingCallWatcher::finished, context: q, slot: [this, watcher] {
268 Q_Q(DropJob);
269
270 if (watcher->isError()) {
271 q->setError(KIO::ERR_UNKNOWN);
272 }
273 q->emitResult();
274 });
275
276 return;
277 }
278#endif
279
280 if (!m_urls.isEmpty()) {
281 if (destIsDirectory()) {
282 handleCopyToDirectory();
283 } else { // local file
284 const QString destFile = m_destUrl.toLocalFile();
285 if (KDesktopFile::isDesktopFile(path: destFile)) {
286 handleDropToDesktopFile();
287 } else if (QFileInfo(destFile).isExecutable()) {
288 handleDropToExecutable();
289 } else {
290 // should not happen, if KDirModel::flags is correct
291 q->setError(KIO::ERR_ACCESS_DENIED);
292 q->emitResult();
293 }
294 }
295 } else if (m_mimeData) {
296 // Dropping raw data
297 KIO::PasteJob *job = KIO::PasteJobPrivate::newJob(mimeData: m_mimeData, destDir: m_destUrl, flags: KIO::HideProgressInfo, clipboard: false /*not clipboard*/);
298 QObject::connect(sender: job, signal: &KIO::PasteJob::itemCreated, context: q, slot: &KIO::DropJob::itemCreated);
299 q->addSubjob(job);
300 }
301}
302
303void DropJobPrivate::fillPopupMenu(KIO::DropMenu *popup)
304{
305 Q_Q(DropJob);
306
307 const int separatorLength = QCoreApplication::translate(context: "QShortcut", key: "+").size();
308 QString seq = QKeySequence(Qt::ShiftModifier).toString(format: QKeySequence::NativeText);
309 seq.chop(n: separatorLength); // chop superfluous '+'
310 QAction *popupMoveAction = new QAction(i18n("&Move Here") + QLatin1Char('\t') + seq, popup);
311 popupMoveAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-move"), fallback: QIcon::fromTheme(QStringLiteral("go-jump"))));
312 popupMoveAction->setData(QVariant::fromValue(value: Qt::MoveAction));
313 seq = QKeySequence(Qt::ControlModifier).toString(format: QKeySequence::NativeText);
314 seq.chop(n: separatorLength);
315
316 const QString copyActionName = m_allSourcesAreHttpUrls ? i18nc("@action:inmenu Download contents of URL here", "&Download Here") : i18n("&Copy Here");
317 const QIcon copyActionIcon = QIcon::fromTheme(name: m_allSourcesAreHttpUrls ? QStringLiteral("download") : QStringLiteral("edit-copy"));
318 QAction *popupCopyAction = new QAction(copyActionName + QLatin1Char('\t') + seq, popup);
319 popupCopyAction->setIcon(copyActionIcon);
320 popupCopyAction->setData(QVariant::fromValue(value: Qt::CopyAction));
321 seq = QKeySequence(Qt::ControlModifier | Qt::ShiftModifier).toString(format: QKeySequence::NativeText);
322 seq.chop(n: separatorLength);
323 QAction *popupLinkAction = new QAction(i18n("&Link Here") + QLatin1Char('\t') + seq, popup);
324 popupLinkAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-link")));
325 popupLinkAction->setData(QVariant::fromValue(value: Qt::LinkAction));
326
327 if (m_possibleActions & Qt::MoveAction) {
328 popup->addAction(action: popupMoveAction);
329 }
330
331 if (m_possibleActions & Qt::CopyAction) {
332 popup->addAction(action: popupCopyAction);
333 }
334
335 popup->addAction(action: popupLinkAction);
336
337 addPluginActions(popup, itemProps: m_itemProps);
338}
339
340void DropJobPrivate::addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps)
341{
342 const QList<KPluginMetaData> plugin_offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/kio_dnd"));
343 for (const KPluginMetaData &data : plugin_offers) {
344 if (auto plugin = KPluginFactory::instantiatePlugin<KIO::DndPopupMenuPlugin>(data).plugin) {
345 const auto actions = plugin->setup(popupMenuInfo: itemProps, destination: m_destUrl);
346 for (auto action : actions) {
347 action->setParent(popup);
348 }
349 m_pluginActions += actions;
350 }
351 }
352
353 popup->addExtraActions(appActions: m_appActions, pluginActions: m_pluginActions);
354}
355
356void DropJob::setApplicationActions(const QList<QAction *> &actions)
357{
358 Q_D(DropJob);
359
360 d->m_appActions = actions;
361
362 for (KIO::DropMenu *menu : std::as_const(t&: d->m_menus)) {
363 menu->addExtraActions(appActions: d->m_appActions, pluginActions: d->m_pluginActions);
364 }
365}
366
367void DropJob::showMenu(const QPoint &p, QAction *atAction)
368{
369 Q_D(DropJob);
370
371 if (!(d->m_dropjobFlags & KIO::ShowMenuManually)) {
372 return;
373 }
374
375 for (KIO::DropMenu *menu : std::as_const(t&: d->m_menus)) {
376 if (QWindow *transientParent = d->transientParent()) {
377 if (menu->winId()) {
378 menu->windowHandle()->setTransientParent(transientParent);
379 }
380 }
381 menu->popup(pos: p, at: atAction);
382 }
383}
384
385void DropJobPrivate::slotTriggered(QAction *action)
386{
387 Q_Q(DropJob);
388 if (m_appActions.contains(t: action) || m_pluginActions.contains(t: action)) {
389 q->emitResult();
390 return;
391 }
392 const QVariant data = action->data();
393 if (!data.canConvert<Qt::DropAction>()) {
394 q->setError(KIO::ERR_USER_CANCELED);
395 q->emitResult();
396 return;
397 }
398 m_dropAction = data.value<Qt::DropAction>();
399 doCopyToDirectory();
400}
401
402void DropJobPrivate::slotAboutToHide()
403{
404 Q_Q(DropJob);
405 // QMenu emits aboutToHide before triggered.
406 // So we need to give the menu time in case it needs to emit triggered.
407 // If it does, the cleanup will be done by slotTriggered.
408 QTimer::singleShot(interval: 0, receiver: q, slot: [=, this]() {
409 if (!m_triggered) {
410 q->setError(KIO::ERR_USER_CANCELED);
411 q->emitResult();
412 }
413 });
414}
415
416void DropJobPrivate::handleCopyToDirectory()
417{
418 Q_Q(DropJob);
419
420 // Process m_dropAction as set by Qt at the time of the drop event
421 if (!KProtocolManager::supportsWriting(url: m_destUrl)) {
422 slotDropActionDetermined(error: KIO::ERR_CANNOT_WRITE);
423 return;
424 }
425
426 if (!m_destItem.isNull() && !m_destItem.isWritable() && (m_flags & KIO::NoPrivilegeExecution)) {
427 slotDropActionDetermined(error: KIO::ERR_WRITE_ACCESS_DENIED);
428 return;
429 }
430
431 // Check what the source can do
432 KFileItemList fileItems;
433 fileItems.reserve(asize: m_urls.size());
434
435 bool allItemsAreFromTrash = true;
436 bool allItemsAreLocal = true;
437 bool allItemsAreSameDevice = true;
438 bool containsTrashRoot = false;
439 bool equalDestination = true;
440 m_allSourcesAreHttpUrls = true;
441 // Check if the default behavior has been changed to MoveAction, read from kdeglobals
442 const KConfigGroup g = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
443 QMetaEnum metaEnum = QMetaEnum::fromType<DndBehavior>();
444 QString configValue = g.readEntry(key: "DndBehavior", aDefault: metaEnum.valueToKey(value: DndBehavior::AlwaysAsk));
445 bool defaultActionIsMove = metaEnum.keyToValue(key: configValue.toLocal8Bit().constData());
446
447 KMountPoint::List mountPoints;
448 bool destIsLocal = m_destUrl.isLocalFile();
449 QString destDevice;
450 if (defaultActionIsMove && destIsLocal) {
451 // As getting the mount point can be slow, only do it when we need to.
452 if (mountPoints.isEmpty()) {
453 mountPoints = KMountPoint::currentMountPoints();
454 }
455 KMountPoint::Ptr destMountPoint = mountPoints.findByPath(path: m_destUrl.path());
456 if (destMountPoint) {
457 destDevice = destMountPoint->mountedFrom();
458 } else {
459 qCWarning(KIO_WIDGETS) << "Could not determine mount point for destination drop target " << m_destUrl;
460 }
461 } else {
462 allItemsAreSameDevice = false;
463 }
464
465 for (const QUrl &url : m_urls) {
466 const bool local = url.isLocalFile();
467 if (!local) {
468 allItemsAreLocal = false;
469 allItemsAreSameDevice = false;
470 }
471#ifdef Q_OS_LINUX
472 // Check if the file is already in the xdg trash folder, BUG:497390
473 const QString xdgtrash = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash");
474 if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
475 if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
476 containsTrashRoot = true;
477 }
478 } else if (local || url.scheme() == QLatin1String("file")) {
479 if (!url.toLocalFile().startsWith(s: xdgtrash)) {
480 allItemsAreFromTrash = false;
481 } else if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
482 containsTrashRoot = true;
483 }
484 } else {
485 allItemsAreFromTrash = false;
486 }
487#else
488 if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
489 if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
490 containsTrashRoot = true;
491 }
492 } else {
493 allItemsAreFromTrash = false;
494 }
495#endif
496
497 if (equalDestination && !m_destUrl.matches(url: url.adjusted(options: QUrl::RemoveFilename), options: QUrl::StripTrailingSlash)) {
498 equalDestination = false;
499 }
500
501 if (defaultActionIsMove && allItemsAreSameDevice) {
502 // As getting the mount point can be slow, only do it when we need to.
503 if (mountPoints.isEmpty()) {
504 mountPoints = KMountPoint::currentMountPoints();
505 }
506 QString sourceDevice;
507 KMountPoint::Ptr sourceMountPoint = mountPoints.findByPath(path: url.path());
508 if (sourceMountPoint) {
509 sourceDevice = sourceMountPoint->mountedFrom();
510 } else {
511 qCWarning(KIO_WIDGETS) << "Could not determine mount point for destination drag source " << url;
512 }
513 if (sourceDevice != destDevice && !KFileItem(url).isLink()) {
514 allItemsAreSameDevice = false;
515 }
516 if (sourceDevice.isEmpty()) {
517 // Sanity check in case we somehow have a local files that we can't get the mount points from.
518 allItemsAreSameDevice = false;
519 }
520 }
521
522 if (m_allSourcesAreHttpUrls && !url.scheme().startsWith(QStringLiteral("http"), cs: Qt::CaseInsensitive)) {
523 m_allSourcesAreHttpUrls = false;
524 }
525
526 fileItems.append(t: KFileItem(url));
527
528 if (url.matches(url: m_destUrl, options: QUrl::StripTrailingSlash)) {
529 slotDropActionDetermined(error: KIO::ERR_DROP_ON_ITSELF);
530 return;
531 }
532 }
533 m_itemProps.setItems(fileItems);
534
535 m_possibleActions |= Qt::LinkAction;
536 const bool sReading = m_itemProps.supportsReading();
537 // For http URLs, even though technically the protocol supports deleting,
538 // this never makes sense for a drag operation.
539 const bool sDeleting = m_allSourcesAreHttpUrls ? false : m_itemProps.supportsDeleting();
540 const bool sMoving = m_itemProps.supportsMoving();
541
542 if (!sReading) {
543 m_possibleActions &= ~Qt::CopyAction;
544 }
545
546 if (!(sMoving || (sReading && sDeleting)) || equalDestination) {
547 m_possibleActions &= ~Qt::MoveAction;
548 }
549
550 const bool trashing = m_destUrl.scheme() == QLatin1String("trash");
551 if (trashing) {
552 if (allItemsAreFromTrash) {
553 qCDebug(KIO_WIDGETS) << "Dropping items from trash to trash";
554 slotDropActionDetermined(error: KIO::ERR_DROP_ON_ITSELF);
555 return;
556 }
557 m_dropAction = Qt::MoveAction;
558
559 auto *askUserInterface = KIO::delegateExtension<AskUserActionInterface *>(job: q);
560
561 // No UI Delegate set for this job, or a delegate that doesn't implement
562 // AskUserActionInterface, then just proceed with the job without asking.
563 // This is useful for non-interactive usage, (which doesn't actually apply
564 // here as a DropJob is always interactive), but this is useful for unittests,
565 // which are typically non-interactive.
566 if (!askUserInterface) {
567 slotDropActionDetermined(error: KJob::NoError);
568 return;
569 }
570
571 QObject::connect(sender: askUserInterface, signal: &KIO::AskUserActionInterface::askUserDeleteResult, context: q, slot: [this](bool allowDelete) {
572 if (allowDelete) {
573 slotDropActionDetermined(error: KJob::NoError);
574 } else {
575 slotDropActionDetermined(error: KIO::ERR_USER_CANCELED);
576 }
577 });
578
579 askUserInterface->askUserDelete(urls: m_urls, deletionType: KIO::AskUserActionInterface::Trash, confirmationType: KIO::AskUserActionInterface::DefaultConfirmation, parent: KJobWidgets::window(job: q));
580 return;
581 }
582
583 // If we can't determine the action below, we use ERR::UNKNOWN as we need to ask
584 // the user via a popup menu.
585 int err = KIO::ERR_UNKNOWN;
586 const bool implicitCopy = m_destUrl.scheme() == QLatin1String("stash");
587 if (implicitCopy) {
588 m_dropAction = Qt::CopyAction;
589 err = KJob::NoError; // Ok
590 } else if (containsTrashRoot) {
591 // Dropping a link to the trash: don't move the full contents, just make a link (#319660)
592 m_dropAction = Qt::LinkAction;
593 err = KJob::NoError; // Ok
594 } else if (allItemsAreFromTrash) {
595 // No point in asking copy/move/link when using dragging from the trash, just move the file out.
596 m_dropAction = Qt::MoveAction;
597 err = KJob::NoError; // Ok
598 } else if (defaultActionIsMove && (m_possibleActions & Qt::MoveAction) && allItemsAreLocal && allItemsAreSameDevice) {
599 if (m_keyboardModifiers == Qt::NoModifier) {
600 m_dropAction = Qt::MoveAction;
601 err = KJob::NoError; // Ok
602 } else if (m_keyboardModifiers == Qt::ShiftModifier) {
603 // the user requests to show the menu
604 err = KIO::ERR_UNKNOWN;
605 } else if (m_keyboardModifiers & (Qt::ControlModifier | Qt::AltModifier)) {
606 // Qt determined m_dropAction from the modifiers
607 err = KJob::NoError; // Ok
608 }
609 } else if (m_keyboardModifiers & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier)) {
610 // Qt determined m_dropAction from the modifiers already
611 err = KJob::NoError; // Ok
612 }
613 slotDropActionDetermined(error: err);
614}
615
616QWindow *DropJobPrivate::transientParent()
617{
618 Q_Q(DropJob);
619
620 if (QWidget *widget = KJobWidgets::window(job: q)) {
621 QWidget *window = widget->window();
622 Q_ASSERT(window);
623 return window->windowHandle();
624 }
625
626 if (QWindow *window = KJobWindows::window(job: q)) {
627 return window;
628 }
629
630 return nullptr;
631}
632
633void DropJobPrivate::slotDropActionDetermined(int error)
634{
635 Q_Q(DropJob);
636
637 if (error == KJob::NoError) {
638 doCopyToDirectory();
639 return;
640 }
641
642 // There was an error, handle it
643 if (error == KIO::ERR_UNKNOWN) {
644 KIO::DropMenu *menu = new KIO::DropMenu();
645 QObject::connect(sender: menu, signal: &QMenu::aboutToHide, context: menu, slot: &QObject::deleteLater);
646
647 // If the user clicks outside the menu, it will be destroyed without emitting the triggered signal.
648 QObject::connect(sender: menu, signal: &QMenu::aboutToHide, context: q, slot: [this]() {
649 slotAboutToHide();
650 });
651
652 fillPopupMenu(popup: menu);
653 Q_EMIT q->popupMenuAboutToShow(itemProps: m_itemProps);
654 QObject::connect(sender: menu, signal: &QMenu::triggered, context: q, slot: [this](QAction *action) {
655 m_triggered = true;
656 slotTriggered(action);
657 });
658
659 if (!(m_dropjobFlags & KIO::ShowMenuManually)) {
660 if (QWindow *parent = transientParent()) {
661 if (menu->winId()) {
662 menu->windowHandle()->setTransientParent(parent);
663 }
664 }
665 auto *window = KJobWidgets::window(job: q);
666 menu->popup(pos: window ? window->mapToGlobal(m_relativePos) : QCursor::pos());
667 }
668 m_menus.insert(value: menu);
669 QObject::connect(sender: menu, signal: &QObject::destroyed, context: q, slot: [this, menu]() {
670 m_menus.remove(value: menu);
671 });
672 } else {
673 q->setError(error);
674 q->emitResult();
675 }
676}
677
678void DropJobPrivate::doCopyToDirectory()
679{
680 Q_Q(DropJob);
681 KIO::CopyJob *job = nullptr;
682 switch (m_dropAction) {
683 case Qt::MoveAction:
684 job = KIO::move(src: m_urls, dest: m_destUrl, flags: m_flags);
685 KIO::FileUndoManager::self()->recordJob(op: m_destUrl.scheme() == QLatin1String("trash") ? KIO::FileUndoManager::Trash : KIO::FileUndoManager::Move,
686 src: m_urls,
687 dst: m_destUrl,
688 job);
689 break;
690 case Qt::CopyAction:
691 job = KIO::copy(src: m_urls, dest: m_destUrl, flags: m_flags);
692 KIO::FileUndoManager::self()->recordCopyJob(copyJob: job);
693 break;
694 case Qt::LinkAction:
695 job = KIO::link(src: m_urls, destDir: m_destUrl, flags: m_flags);
696 KIO::FileUndoManager::self()->recordCopyJob(copyJob: job);
697 break;
698 default:
699 qCWarning(KIO_WIDGETS) << "Unknown drop action" << int(m_dropAction);
700 q->setError(KIO::ERR_UNSUPPORTED_ACTION);
701 q->emitResult();
702 return;
703 }
704 Q_ASSERT(job);
705 job->setParentJob(q);
706 job->setMetaData(m_metaData);
707 QObject::connect(sender: job, signal: &KIO::CopyJob::copyingDone, context: q, slot: [q](KIO::Job *, const QUrl &, const QUrl &to) {
708 Q_EMIT q->itemCreated(url: to);
709 });
710 QObject::connect(sender: job, signal: &KIO::CopyJob::copyingLinkDone, context: q, slot: [q](KIO::Job *, const QUrl &, const QString &, const QUrl &to) {
711 Q_EMIT q->itemCreated(url: to);
712 });
713 q->addSubjob(job);
714
715 Q_EMIT q->copyJobStarted(job);
716}
717
718void DropJobPrivate::handleDropToDesktopFile()
719{
720 Q_Q(DropJob);
721 const QString urlKey = QStringLiteral("URL");
722 const QString destFile = m_destUrl.toLocalFile();
723 const KDesktopFile desktopFile(destFile);
724 const KConfigGroup desktopGroup = desktopFile.desktopGroup();
725 if (desktopFile.hasApplicationType()) {
726 // Drop to application -> start app with urls as argument
727 KService::Ptr service(new KService(destFile));
728 // Can't use setParentJob() because ApplicationLauncherJob isn't a KIO::Job,
729 // instead pass q as parent so that KIO::delegateExtension() can find a delegate
730 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q);
731 job->setUrls(m_urls);
732 QObject::connect(sender: job, signal: &KJob::result, context: q, slot: [=]() {
733 if (job->error()) {
734 q->setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
735 q->setErrorText(destFile);
736 }
737 q->emitResult();
738 });
739 job->start();
740 } else if (desktopFile.hasLinkType() && desktopGroup.hasKey(key: urlKey)) {
741 // Drop to link -> adjust destination directory
742 m_destUrl = QUrl::fromUserInput(userInput: desktopGroup.readPathEntry(pKey: urlKey, aDefault: QString()));
743 handleCopyToDirectory();
744 } else {
745 if (desktopFile.hasDeviceType()) {
746 qCWarning(KIO_WIDGETS) << "Not re-implemented; please email kde-frameworks-devel@kde.org if you need this.";
747 // take code from libkonq's old konq_operations.cpp
748 // for now, fallback
749 }
750 // Some other kind of .desktop file (service, servicetype...)
751 q->setError(KIO::ERR_UNSUPPORTED_ACTION);
752 q->emitResult();
753 }
754}
755
756void DropJobPrivate::handleDropToExecutable()
757{
758 Q_Q(DropJob);
759 // Launch executable for each of the files
760 QStringList args;
761 args.reserve(asize: m_urls.size());
762 for (const QUrl &url : std::as_const(t: m_urls)) {
763 args << url.toLocalFile(); // assume local files
764 }
765 QProcess::startDetached(program: m_destUrl.toLocalFile(), arguments: args);
766 q->emitResult();
767}
768
769void DropJob::slotResult(KJob *job)
770{
771 if (job->error()) {
772 KIO::Job::slotResult(job); // will set the error and emit result(this)
773 return;
774 }
775 removeSubjob(job);
776 emitResult();
777}
778
779DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags)
780{
781 return DropJobPrivate::newJob(dropEvent, destUrl, dropjobFlags: KIO::DropJobDefaultFlags, flags);
782}
783
784DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
785{
786 return DropJobPrivate::newJob(dropEvent, destUrl, dropjobFlags, flags);
787}
788
789#include "dropjob.moc"
790#include "moc_dropjob.cpp"
791

source code of kio/src/widgets/dropjob.cpp