KIO

dropjob.cpp
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{
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(m_mimeData, KUrlMimeData::PreferLocalUrls, &m_metaData))
90 , m_dropAction(dropEvent->dropAction())
91 , m_relativePos(dropEvent->position().toPoint())
92 , m_keyboardModifiers(dropEvent->modifiers())
93 , m_hasArkFormat(m_mimeData->hasFormat(s_applicationSlashXDashKDEDashArkDashDnDExtractDashService)
94 && m_mimeData->hasFormat(s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath))
95 , m_destUrl(destUrl)
96 , m_destItem(KCoreDirLister::cachedItemForUrl(destUrl))
97 , m_flags(flags)
98 , m_dropjobFlags(dropjobFlags)
99 , m_triggered(false)
100 {
101 // Check for the drop of a bookmark -> we want a Link action
102 if (m_mimeData->hasFormat(QStringLiteral("application/x-xbel"))) {
104 m_dropAction = Qt::LinkAction;
105 }
106 if (m_destItem.isNull() && m_destUrl.isLocalFile()) {
107 m_destItem = KFileItem(m_destUrl);
108 }
109
110 if (m_hasArkFormat) {
111 m_remoteArkDBusClient = QString::fromUtf8(m_mimeData->data(s_applicationSlashXDashKDEDashArkDashDnDExtractDashService));
112 m_remoteArkDBusPath = QString::fromUtf8(m_mimeData->data(s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath));
113 }
114
115 if (!(m_flags & KIO::NoPrivilegeExecution)) {
116 m_privilegeExecutionEnabled = true;
117 switch (m_dropAction) {
118 case Qt::CopyAction:
119 m_operationType = Copy;
120 break;
121 case Qt::MoveAction:
122 m_operationType = Move;
123 break;
124 case Qt::LinkAction:
125 m_operationType = Symlink;
126 break;
127 default:
128 m_operationType = Other;
129 break;
130 }
131 }
132 }
133
134 bool destIsDirectory() const
135 {
136 if (!m_destItem.isNull()) {
137 return m_destItem.isDir();
138 }
139 // We support local dir, remote dir, local desktop file, local executable.
140 // So for remote URLs, we just assume they point to a directory, the user will get an error from KIO::copy if not.
141 return true;
142 }
143 void handleCopyToDirectory();
144 void slotDropActionDetermined(int error);
145 void handleDropToDesktopFile();
146 void handleDropToExecutable();
147 void fillPopupMenu(KIO::DropMenu *popup);
148 void addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps);
149 void doCopyToDirectory();
150
151 QWindow *transientParent();
152
153 QPointer<const QMimeData> m_mimeData;
154 const QList<QUrl> m_urls;
155 QMap<QString, QString> m_metaData;
156 Qt::DropAction m_dropAction;
157 Qt::DropActions m_possibleActions;
158 bool m_allSourcesAreHttpUrls;
159 QPoint m_relativePos;
160 Qt::KeyboardModifiers m_keyboardModifiers;
161 KFileItemListProperties m_itemProps;
162 bool m_hasArkFormat;
163 QString m_remoteArkDBusClient;
164 QString m_remoteArkDBusPath;
165 QUrl m_destUrl;
166 KFileItem m_destItem; // null for remote URLs not found in the dirlister cache
167 const JobFlags m_flags;
168 const DropJobFlags m_dropjobFlags;
169 QList<QAction *> m_appActions;
170 QList<QAction *> m_pluginActions;
171 bool m_triggered; // Tracks whether an action has been triggered in the popup menu.
172 QSet<KIO::DropMenu *> m_menus;
173
174 Q_DECLARE_PUBLIC(DropJob)
175
176 void slotStart();
177 void slotTriggered(QAction *);
178 void slotAboutToHide();
179
180 static inline DropJob *newJob(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
181 {
182 DropJob *job = new DropJob(*new DropJobPrivate(dropEvent, destUrl, dropjobFlags, flags));
184 // Note: never KIO::getJobTracker()->registerJob here.
185 // We don't want a progress dialog during the copy/move/link popup, it would in fact close
186 // the popup
187 return job;
188 }
189};
190
191DropMenu::DropMenu(QWidget *parent)
192 : QMenu(parent)
193 , m_extraActionsSeparator(nullptr)
194{
195 m_cancelAction = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(QKeySequence::NativeText), this);
196 m_cancelAction->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
197
198 m_lastSeparator = new QAction(this);
199 m_lastSeparator->setSeparator(true);
200}
201
202DropMenu::~DropMenu()
203{
204}
205
206void DropMenu::addExtraActions(const QList<QAction *> &appActions, const QList<QAction *> &pluginActions)
207{
208 removeAction(m_lastSeparator);
209 removeAction(m_cancelAction);
210
211 removeAction(m_extraActionsSeparator);
212 for (QAction *action : std::as_const(m_appActions)) {
213 removeAction(action);
214 }
215 for (QAction *action : std::as_const(m_pluginActions)) {
216 removeAction(action);
217 }
218
219 m_appActions = appActions;
220 m_pluginActions = pluginActions;
221
222 if (!m_appActions.isEmpty() || !m_pluginActions.isEmpty()) {
223 QAction *firstExtraAction = m_appActions.value(0, m_pluginActions.value(0, nullptr));
224 if (firstExtraAction && !firstExtraAction->isSeparator()) {
225 if (!m_extraActionsSeparator) {
226 m_extraActionsSeparator = new QAction(this);
227 m_extraActionsSeparator->setSeparator(true);
228 }
229 addAction(m_extraActionsSeparator);
230 }
231 addActions(appActions);
232 addActions(pluginActions);
233 }
234
235 addAction(m_lastSeparator);
236 addAction(m_cancelAction);
237}
238
239DropJob::DropJob(DropJobPrivate &dd)
240 : Job(dd)
241{
242 Q_D(DropJob);
243
244 QTimer::singleShot(0, this, [d]() {
245 d->slotStart();
246 });
247}
248
249DropJob::~DropJob()
250{
251}
252
253void DropJobPrivate::slotStart()
254{
255 Q_Q(DropJob);
256
257#ifdef WITH_QTDBUS
258 if (m_hasArkFormat) {
259 QDBusMessage message = QDBusMessage::createMethodCall(m_remoteArkDBusClient,
260 m_remoteArkDBusPath,
261 QStringLiteral("org.kde.ark.DndExtract"),
262 QStringLiteral("extractSelectedFilesTo"));
263 message.setArguments({m_destUrl.toDisplayString(QUrl::PreferLocalFile)});
264 const auto pending = QDBusConnection::sessionBus().asyncCall(message);
265 auto watcher = std::make_shared<QDBusPendingCallWatcher>(pending);
266 QObject::connect(watcher.get(), &QDBusPendingCallWatcher::finished, q, [this, watcher] {
267 Q_Q(DropJob);
268
269 if (watcher->isError()) {
270 q->setError(KIO::ERR_UNKNOWN);
271 }
272 q->emitResult();
273 });
274
275 return;
276 }
277#endif
278
279 if (!m_urls.isEmpty()) {
280 if (destIsDirectory()) {
281 handleCopyToDirectory();
282 } else { // local file
283 const QString destFile = m_destUrl.toLocalFile();
284 if (KDesktopFile::isDesktopFile(destFile)) {
285 handleDropToDesktopFile();
286 } else if (QFileInfo(destFile).isExecutable()) {
287 handleDropToExecutable();
288 } else {
289 // should not happen, if KDirModel::flags is correct
290 q->setError(KIO::ERR_ACCESS_DENIED);
291 q->emitResult();
292 }
293 }
294 } else if (m_mimeData) {
295 // Dropping raw data
296 KIO::PasteJob *job = KIO::PasteJobPrivate::newJob(m_mimeData, m_destUrl, KIO::HideProgressInfo, false /*not clipboard*/);
298 q->addSubjob(job);
299 }
300}
301
302void DropJobPrivate::fillPopupMenu(KIO::DropMenu *popup)
303{
304 Q_Q(DropJob);
305
306 Q_EMIT q->popupMenuAboutToShow(m_itemProps);
307
308 const int separatorLength = QCoreApplication::translate("QShortcut", "+").size();
309 QString seq = QKeySequence(Qt::ShiftModifier).toString(QKeySequence::NativeText);
310 seq.chop(separatorLength); // chop superfluous '+'
311 QAction *popupMoveAction = new QAction(i18n("&Move Here") + QLatin1Char('\t') + seq, popup);
312 popupMoveAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-move"), QIcon::fromTheme(QStringLiteral("go-jump"))));
313 popupMoveAction->setData(QVariant::fromValue(Qt::MoveAction));
314 seq = QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText);
315 seq.chop(separatorLength);
316
317 const QString copyActionName = m_allSourcesAreHttpUrls ? i18nc("@action:inmenu Download contents of URL here", "&Download Here") : i18n("&Copy Here");
318 const QIcon copyActionIcon = QIcon::fromTheme(m_allSourcesAreHttpUrls ? QStringLiteral("download") : QStringLiteral("edit-copy"));
319 QAction *popupCopyAction = new QAction(copyActionName + QLatin1Char('\t') + seq, popup);
320 popupCopyAction->setIcon(copyActionIcon);
321 popupCopyAction->setData(QVariant::fromValue(Qt::CopyAction));
322 seq = QKeySequence(Qt::ControlModifier | Qt::ShiftModifier).toString(QKeySequence::NativeText);
323 seq.chop(separatorLength);
324 QAction *popupLinkAction = new QAction(i18n("&Link Here") + QLatin1Char('\t') + seq, popup);
325 popupLinkAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-link")));
326 popupLinkAction->setData(QVariant::fromValue(Qt::LinkAction));
327
328 if (m_possibleActions & Qt::MoveAction) {
329 popup->addAction(popupMoveAction);
330 }
331
332 if (m_possibleActions & Qt::CopyAction) {
333 popup->addAction(popupCopyAction);
334 }
335
336 popup->addAction(popupLinkAction);
337
338 addPluginActions(popup, m_itemProps);
339}
340
341void DropJobPrivate::addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps)
342{
343 const QList<KPluginMetaData> plugin_offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/kio_dnd"));
344 for (const KPluginMetaData &data : plugin_offers) {
345 if (auto plugin = KPluginFactory::instantiatePlugin<KIO::DndPopupMenuPlugin>(data).plugin) {
346 const auto actions = plugin->setup(itemProps, m_destUrl);
347 for (auto action : actions) {
348 action->setParent(popup);
349 }
350 m_pluginActions += actions;
351 }
352 }
353
354 popup->addExtraActions(m_appActions, m_pluginActions);
355}
356
358{
359 Q_D(DropJob);
360
361 d->m_appActions = actions;
362
363 for (KIO::DropMenu *menu : std::as_const(d->m_menus)) {
364 menu->addExtraActions(d->m_appActions, d->m_pluginActions);
365 }
366}
367
368void DropJob::showMenu(const QPoint &p, QAction *atAction)
369{
370 Q_D(DropJob);
371
372 if (!(d->m_dropjobFlags & KIO::ShowMenuManually)) {
373 return;
374 }
375
376 for (KIO::DropMenu *menu : std::as_const(d->m_menus)) {
377 if (QWindow *transientParent = d->transientParent()) {
378 if (menu->winId()) {
379 menu->windowHandle()->setTransientParent(transientParent);
380 }
381 }
382 menu->popup(p, atAction);
383 }
384}
385
386void DropJobPrivate::slotTriggered(QAction *action)
387{
388 Q_Q(DropJob);
389 if (m_appActions.contains(action) || m_pluginActions.contains(action)) {
390 q->emitResult();
391 return;
392 }
393 const QVariant data = action->data();
394 if (!data.canConvert<Qt::DropAction>()) {
395 q->setError(KIO::ERR_USER_CANCELED);
396 q->emitResult();
397 return;
398 }
399 m_dropAction = data.value<Qt::DropAction>();
400 doCopyToDirectory();
401}
402
403void DropJobPrivate::slotAboutToHide()
404{
405 Q_Q(DropJob);
406 // QMenu emits aboutToHide before triggered.
407 // So we need to give the menu time in case it needs to emit triggered.
408 // If it does, the cleanup will be done by slotTriggered.
409 QTimer::singleShot(0, q, [=, this]() {
410 if (!m_triggered) {
411 q->setError(KIO::ERR_USER_CANCELED);
412 q->emitResult();
413 }
414 });
415}
416
417void DropJobPrivate::handleCopyToDirectory()
418{
419 Q_Q(DropJob);
420
421 // Process m_dropAction as set by Qt at the time of the drop event
422 if (!KProtocolManager::supportsWriting(m_destUrl)) {
423 slotDropActionDetermined(KIO::ERR_CANNOT_WRITE);
424 return;
425 }
426
427 if (!m_destItem.isNull() && !m_destItem.isWritable() && (m_flags & KIO::NoPrivilegeExecution)) {
428 slotDropActionDetermined(KIO::ERR_WRITE_ACCESS_DENIED);
429 return;
430 }
431
432 // Check what the source can do
433 KFileItemList fileItems;
434 fileItems.reserve(m_urls.size());
435
436 bool allItemsAreFromTrash = true;
437 bool allItemsAreLocal = true;
438 bool allItemsAreSameDevice = true;
439 bool containsTrashRoot = false;
440 bool equalDestination = true;
441 m_allSourcesAreHttpUrls = true;
442 // Check if the default behavior has been changed to MoveAction, read from kdeglobals
443 const KConfigGroup g = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
444 QMetaEnum metaEnum = QMetaEnum::fromType<DndBehavior>();
445 QString configValue = g.readEntry("DndBehavior", metaEnum.valueToKey(DndBehavior::AlwaysAsk));
446 bool defaultActionIsMove = metaEnum.keyToValue(configValue.toLocal8Bit().constData());
447
448 KMountPoint::List mountPoints;
449 bool destIsLocal = m_destUrl.isLocalFile();
450 QString destDevice;
451 if (defaultActionIsMove && destIsLocal) {
452 // As getting the mount point can be slow, only do it when we need to.
453 if (mountPoints.isEmpty()) {
454 mountPoints = KMountPoint::currentMountPoints();
455 }
456 destDevice = mountPoints.findByPath(m_destUrl.path())->mountedFrom();
457 } else {
458 allItemsAreSameDevice = false;
459 }
460
461 for (const QUrl &url : m_urls) {
462 const bool local = url.isLocalFile();
463 if (!local) {
464 allItemsAreLocal = false;
465 allItemsAreSameDevice = false;
466 }
467#ifdef Q_OS_LINUX
468 // Check if the file is already in the xdg trash folder, BUG:497390
469 const QString xdgtrash = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash");
470 if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
471 if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
472 containsTrashRoot = true;
473 }
474 } else if (local || url.scheme() == QLatin1String("file")) {
475 if (!url.toLocalFile().startsWith(xdgtrash)) {
476 allItemsAreFromTrash = false;
477 } else if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
478 containsTrashRoot = true;
479 }
480 } else {
481 allItemsAreFromTrash = false;
482 }
483#else
484 if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
485 if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
486 containsTrashRoot = true;
487 }
488 } else {
489 allItemsAreFromTrash = false;
490 }
491#endif
492
493 if (equalDestination && !m_destUrl.matches(url.adjusted(QUrl::RemoveFilename), QUrl::StripTrailingSlash)) {
494 equalDestination = false;
495 }
496
497 if (defaultActionIsMove && allItemsAreSameDevice) {
498 // As getting the mount point can be slow, only do it when we need to.
499 if (mountPoints.isEmpty()) {
500 mountPoints = KMountPoint::currentMountPoints();
501 }
502 const QString &sourceDevice = mountPoints.findByPath(url.path())->mountedFrom();
503 if (sourceDevice != destDevice && !KFileItem(url).isLink()) {
504 allItemsAreSameDevice = false;
505 } else if (sourceDevice.isEmpty()) {
506 // Sanity check in case we somehow have a local files that we can't get the mount points from.
507 allItemsAreSameDevice = false;
508 }
509 }
510
511 if (m_allSourcesAreHttpUrls && !url.scheme().startsWith(QStringLiteral("http"), Qt::CaseInsensitive)) {
512 m_allSourcesAreHttpUrls = false;
513 }
514
515 fileItems.append(KFileItem(url));
516
517 if (url.matches(m_destUrl, QUrl::StripTrailingSlash)) {
518 slotDropActionDetermined(KIO::ERR_DROP_ON_ITSELF);
519 return;
520 }
521 }
522 m_itemProps.setItems(fileItems);
523
524 m_possibleActions = Qt::LinkAction;
525 const bool sReading = m_itemProps.supportsReading();
526 // For http URLs, even though technically the protocol supports deleting,
527 // this never makes sense for a drag operation.
528 const bool sDeleting = m_allSourcesAreHttpUrls ? false : m_itemProps.supportsDeleting();
529 const bool sMoving = m_itemProps.supportsMoving();
530
531 if (sReading) {
532 m_possibleActions |= Qt::CopyAction;
533 }
534
535 if (sMoving || (sReading && sDeleting)) {
536 if (!equalDestination) {
537 m_possibleActions |= Qt::MoveAction;
538 }
539 }
540
541 const bool trashing = m_destUrl.scheme() == QLatin1String("trash");
542 if (trashing) {
543 if (allItemsAreFromTrash) {
544 qCDebug(KIO_WIDGETS) << "Dropping items from trash to trash";
545 slotDropActionDetermined(KIO::ERR_DROP_ON_ITSELF);
546 return;
547 }
548 m_dropAction = Qt::MoveAction;
549
550 auto *askUserInterface = KIO::delegateExtension<AskUserActionInterface *>(q);
551
552 // No UI Delegate set for this job, or a delegate that doesn't implement
553 // AskUserActionInterface, then just proceed with the job without asking.
554 // This is useful for non-interactive usage, (which doesn't actually apply
555 // here as a DropJob is always interactive), but this is useful for unittests,
556 // which are typically non-interactive.
557 if (!askUserInterface) {
558 slotDropActionDetermined(KJob::NoError);
559 return;
560 }
561
562 QObject::connect(askUserInterface, &KIO::AskUserActionInterface::askUserDeleteResult, q, [this](bool allowDelete) {
563 if (allowDelete) {
564 slotDropActionDetermined(KJob::NoError);
565 } else {
566 slotDropActionDetermined(KIO::ERR_USER_CANCELED);
567 }
568 });
569
571 return;
572 }
573
574 // If we can't determine the action below, we use ERR::UNKNOWN as we need to ask
575 // the user via a popup menu.
576 int err = KIO::ERR_UNKNOWN;
577 const bool implicitCopy = m_destUrl.scheme() == QLatin1String("stash");
578 if (implicitCopy) {
579 m_dropAction = Qt::CopyAction;
580 err = KJob::NoError; // Ok
581 } else if (containsTrashRoot) {
582 // Dropping a link to the trash: don't move the full contents, just make a link (#319660)
583 m_dropAction = Qt::LinkAction;
584 err = KJob::NoError; // Ok
585 } else if (allItemsAreFromTrash) {
586 // No point in asking copy/move/link when using dragging from the trash, just move the file out.
587 m_dropAction = Qt::MoveAction;
588 err = KJob::NoError; // Ok
589 } else if (defaultActionIsMove && (m_possibleActions & Qt::MoveAction) && allItemsAreLocal && allItemsAreSameDevice) {
590 if (m_keyboardModifiers == Qt::NoModifier) {
591 m_dropAction = Qt::MoveAction;
592 err = KJob::NoError; // Ok
593 } else if (m_keyboardModifiers == Qt::ShiftModifier) {
594 // the user requests to show the menu
595 err = KIO::ERR_UNKNOWN;
596 } else if (m_keyboardModifiers & (Qt::ControlModifier | Qt::AltModifier)) {
597 // Qt determined m_dropAction from the modifiers
598 err = KJob::NoError; // Ok
599 }
600 } else if (m_keyboardModifiers & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier)) {
601 // Qt determined m_dropAction from the modifiers already
602 err = KJob::NoError; // Ok
603 }
604 slotDropActionDetermined(err);
605}
606
607QWindow *DropJobPrivate::transientParent()
608{
609 Q_Q(DropJob);
610
611 if (QWidget *widget = KJobWidgets::window(q)) {
612 QWidget *window = widget->window();
613 Q_ASSERT(window);
614 return window->windowHandle();
615 }
616
617 if (QWindow *window = KJobWindows::window(q)) {
618 return window;
619 }
620
621 return nullptr;
622}
623
624void DropJobPrivate::slotDropActionDetermined(int error)
625{
626 Q_Q(DropJob);
627
628 if (error == KJob::NoError) {
629 doCopyToDirectory();
630 return;
631 }
632
633 // There was an error, handle it
634 if (error == KIO::ERR_UNKNOWN) {
635 KIO::DropMenu *menu = new KIO::DropMenu();
637
638 // If the user clicks outside the menu, it will be destroyed without emitting the triggered signal.
639 QObject::connect(menu, &QMenu::aboutToHide, q, [this]() {
640 slotAboutToHide();
641 });
642
643 fillPopupMenu(menu);
644 QObject::connect(menu, &QMenu::triggered, q, [this](QAction *action) {
645 m_triggered = true;
646 slotTriggered(action);
647 });
648
649 if (!(m_dropjobFlags & KIO::ShowMenuManually)) {
650 if (QWindow *parent = transientParent()) {
651 if (menu->winId()) {
652 menu->windowHandle()->setTransientParent(parent);
653 }
654 }
655 auto *window = KJobWidgets::window(q);
656 menu->popup(window ? window->mapToGlobal(m_relativePos) : QCursor::pos());
657 }
658 m_menus.insert(menu);
659 QObject::connect(menu, &QObject::destroyed, q, [this, menu]() {
660 m_menus.remove(menu);
661 });
662 } else {
663 q->setError(error);
664 q->emitResult();
665 }
666}
667
668void DropJobPrivate::doCopyToDirectory()
669{
670 Q_Q(DropJob);
671 KIO::CopyJob *job = nullptr;
672 switch (m_dropAction) {
673 case Qt::MoveAction:
674 job = KIO::move(m_urls, m_destUrl, m_flags);
675 KIO::FileUndoManager::self()->recordJob(m_destUrl.scheme() == QLatin1String("trash") ? KIO::FileUndoManager::Trash : KIO::FileUndoManager::Move,
676 m_urls,
677 m_destUrl,
678 job);
679 break;
680 case Qt::CopyAction:
681 job = KIO::copy(m_urls, m_destUrl, m_flags);
683 break;
684 case Qt::LinkAction:
685 job = KIO::link(m_urls, m_destUrl, m_flags);
687 break;
688 default:
689 qCWarning(KIO_WIDGETS) << "Unknown drop action" << int(m_dropAction);
690 q->setError(KIO::ERR_UNSUPPORTED_ACTION);
691 q->emitResult();
692 return;
693 }
694 Q_ASSERT(job);
695 job->setParentJob(q);
696 job->setMetaData(m_metaData);
697 QObject::connect(job, &KIO::CopyJob::copyingDone, q, [q](KIO::Job *, const QUrl &, const QUrl &to) {
698 Q_EMIT q->itemCreated(to);
699 });
700 QObject::connect(job, &KIO::CopyJob::copyingLinkDone, q, [q](KIO::Job *, const QUrl &, const QString &, const QUrl &to) {
701 Q_EMIT q->itemCreated(to);
702 });
703 q->addSubjob(job);
704
705 Q_EMIT q->copyJobStarted(job);
706}
707
708void DropJobPrivate::handleDropToDesktopFile()
709{
710 Q_Q(DropJob);
711 const QString urlKey = QStringLiteral("URL");
712 const QString destFile = m_destUrl.toLocalFile();
713 const KDesktopFile desktopFile(destFile);
714 const KConfigGroup desktopGroup = desktopFile.desktopGroup();
715 if (desktopFile.hasApplicationType()) {
716 // Drop to application -> start app with urls as argument
717 KService::Ptr service(new KService(destFile));
718 // Can't use setParentJob() because ApplicationLauncherJob isn't a KIO::Job,
719 // instead pass q as parent so that KIO::delegateExtension() can find a delegate
720 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q);
721 job->setUrls(m_urls);
722 QObject::connect(job, &KJob::result, q, [=]() {
723 if (job->error()) {
724 q->setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
725 q->setErrorText(destFile);
726 }
727 q->emitResult();
728 });
729 job->start();
730 } else if (desktopFile.hasLinkType() && desktopGroup.hasKey(urlKey)) {
731 // Drop to link -> adjust destination directory
732 m_destUrl = QUrl::fromUserInput(desktopGroup.readPathEntry(urlKey, QString()));
733 handleCopyToDirectory();
734 } else {
735 if (desktopFile.hasDeviceType()) {
736 qCWarning(KIO_WIDGETS) << "Not re-implemented; please email kde-frameworks-devel@kde.org if you need this.";
737 // take code from libkonq's old konq_operations.cpp
738 // for now, fallback
739 }
740 // Some other kind of .desktop file (service, servicetype...)
741 q->setError(KIO::ERR_UNSUPPORTED_ACTION);
742 q->emitResult();
743 }
744}
745
746void DropJobPrivate::handleDropToExecutable()
747{
748 Q_Q(DropJob);
749 // Launch executable for each of the files
750 QStringList args;
751 args.reserve(m_urls.size());
752 for (const QUrl &url : std::as_const(m_urls)) {
753 args << url.toLocalFile(); // assume local files
754 }
755 QProcess::startDetached(m_destUrl.toLocalFile(), args);
756 q->emitResult();
757}
758
759void DropJob::slotResult(KJob *job)
760{
761 if (job->error()) {
762 KIO::Job::slotResult(job); // will set the error and emit result(this)
763 return;
764 }
765 removeSubjob(job);
766 emitResult();
767}
768
769DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags)
770{
771 return DropJobPrivate::newJob(dropEvent, destUrl, KIO::DropJobDefaultFlags, flags);
772}
773
774DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
775{
776 return DropJobPrivate::newJob(dropEvent, destUrl, dropjobFlags, flags);
777}
778
779#include "dropjob.moc"
780#include "moc_dropjob.cpp"
virtual void slotResult(KJob *job)
bool hasKey(const char *key) const
QString readPathEntry(const char *key, const QString &aDefault) const
QString readEntry(const char *key, const char *aDefault=nullptr) const
static bool isDesktopFile(const QString &path)
void start() override
Starts the job.
void setUrls(const QList< QUrl > &urls)
Specifies the URLs to be passed to the application.
void askUserDeleteResult(bool allowDelete, const QList< QUrl > &urls, KIO::AskUserActionInterface::DeletionType deletionType, QWidget *parent)
Implementations of this interface must emit this signal when the dialog invoked by askUserDelete() fi...
@ Trash
Delete the files/directories directly, i.e. without moving them to Trash.
@ DefaultConfirmation
Do not ask if the user has previously set the "Do not ask again" checkbox (which is is shown in the m...
void copyingLinkDone(KIO::Job *job, const QUrl &from, const QString &target, const QUrl &to)
The job is copying or moving a symbolic link, that points to target.
void copyingDone(KIO::Job *job, const QUrl &from, const QUrl &to, const QDateTime &mtime, bool directory, bool renamed)
The job emits this signal when copying or moving a file or directory successfully finished.
A KIO job that handles dropping into a file-manager-like view.
Definition dropjob.h:66
void setApplicationActions(const QList< QAction * > &actions)
Allows the application to set additional actions in the drop popup menu.
Definition dropjob.cpp:357
void itemCreated(const QUrl &url)
Signals that a file or directory was created.
void showMenu(const QPoint &p, QAction *atAction=nullptr)
Allows the application to show the menu manually.
Definition dropjob.cpp:368
void recordCopyJob(KIO::CopyJob *copyJob)
Record this CopyJob while it's happening and add a command for it so that the user can undo it.
static FileUndoManager * self()
void recordJob(CommandType op, const QList< QUrl > &src, const QUrl &dst, KIO::Job *job)
Record this job while it's happening and add a command for it so that the user can undo it.
The base class for all jobs.
Definition job_base.h:45
void setMetaData(const KIO::MetaData &metaData)
Set meta data to be sent to the worker, replacing existing meta data.
Definition job.cpp:215
void setParentJob(Job *parentJob)
Set the parent Job.
Definition job.cpp:192
bool removeSubjob(KJob *job) override
Mark a sub job as being done.
Definition job.cpp:80
void itemCreated(const QUrl &url)
Signals that a file or directory was created.
void emitResult()
int error() const
void result(KJob *job)
void setUiDelegate(KJobUiDelegate *delegate)
Ptr findByPath(const QString &path) const
Find the mountpoint on which resides path For instance if /home is a separate partition,...
static List currentMountPoints(DetailsNeededFlags infoNeeded=BasicInfoNeeded)
Returns a list of all current mountpoints.
QString mountedFrom() const
Where this filesystem gets mounted from.
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
static bool supportsWriting(const QUrl &url)
Returns whether the protocol can store data to URLs.
QExplicitlySharedDataPointer< KService > Ptr
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
A namespace for KIO globals.
KIOCORE_EXPORT CopyJob * move(const QUrl &src, const QUrl &dest, JobFlags flags=DefaultFlags)
Moves a file or directory src to the given destination dest.
Definition copyjob.cpp:2657
KIOCORE_EXPORT CopyJob * link(const QUrl &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
Create a link.
Definition copyjob.cpp:2691
@ ShowMenuManually
show the menu manually with DropJob::showMenu
Definition dropjob.h:30
QFlags< DropJobFlag > DropJobFlags
Stores a combination of DropJobFlag values.
Definition dropjob.h:48
T delegateExtension(KJob *job)
Returns the child of the job's uiDelegate() that implements the given extension, or nullptr if none w...
KIOCORE_EXPORT CopyJob * copy(const QUrl &src, const QUrl &dest, JobFlags flags=DefaultFlags)
Copy a file or directory src into the destination dest, which can be a file (including the final file...
Definition copyjob.cpp:2635
KIOCORE_EXPORT KJobUiDelegate * createDefaultJobUiDelegate()
Convenience method: use default factory, if there's one, to create a delegate and return it.
KIOWIDGETS_EXPORT DropJob * drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags=DefaultFlags)
Drops the clipboard contents.
Definition dropjob.cpp:769
QFlags< JobFlag > JobFlags
Stores a combination of JobFlag values.
Definition job_base.h:281
@ HideProgressInfo
Hide progress information dialog, i.e. don't show a GUI.
Definition job_base.h:251
@ NoPrivilegeExecution
When set, notifies the worker that application/job does not want privilege execution.
Definition job_base.h:276
@ ERR_DROP_ON_ITSELF
from KIO::DropJob,
Definition global.h:194
QWidget * window(QObject *job)
KGUIADDONS_EXPORT QWindow * window(QObject *job)
KCOREADDONS_EXPORT QList< QUrl > urlsFromMimeData(const QMimeData *mimeData, DecodeOptions decodeOptions=PreferKdeUrls, MetaDataMap *metaData=nullptr)
QVariant data() const const
void setIcon(const QIcon &icon)
bool isSeparator() const const
void setData(const QVariant &data)
const char * constData() const const
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
QPoint pos()
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusConnection sessionBus()
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
void setArguments(const QList< QVariant > &arguments)
void finished(QDBusPendingCallWatcher *self)
QIcon fromTheme(const QString &name)
void append(QList< T > &&value)
bool contains(const AT &value) const const
bool isEmpty() const const
void reserve(qsizetype size)
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
void aboutToHide()
void popup(const QPoint &p, QAction *atAction)
void triggered(QAction *action)
QMetaEnum fromType()
int keyToValue(const char *key, bool *ok) const const
const char * valueToKey(int value) const const
Q_OBJECTQ_OBJECT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void destroyed(QObject *obj)
QObject * parent() const const
bool startDetached(const QString &program, const QStringList &arguments, const QString &workingDirectory, qint64 *pid)
QString writableLocation(StandardLocation type)
void chop(qsizetype n)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
qsizetype size() const const
QByteArray toLocal8Bit() const const
CaseInsensitive
DropAction
Key_Escape
typedef KeyboardModifiers
PreferLocalFile
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
QString toLocalFile() const const
bool canConvert() const const
QVariant fromValue(T &&value)
T value() const const
QWidget(QWidget *parent, Qt::WindowFlags f)
void addActions(const QList< QAction * > &actions)
QPoint mapToGlobal(const QPoint &pos) const const
void removeAction(QAction *action)
WId winId() const const
QWidget * window() const const
QWindow * windowHandle() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:02:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.