Change in kio[master]: New job: KIO::DropJob *KIO::drop(QDropEvent* ev, QUrl destUrl).

David Faure (Code Review) noreply at kde.org
Mon Dec 15 09:38:55 UTC 2014


David Faure has uploaded a new change for review.

  https://gerrit.vesnicky.cesnet.cz/r/241

Change subject: New job: KIO::DropJob *KIO::drop(QDropEvent* ev, QUrl destUrl).
......................................................................

New job: KIO::DropJob *KIO::drop(QDropEvent* ev, QUrl destUrl).

Takes care of the Move/Copy/Link/[app/plugin actions]/Cancel popup,
dropping onto executables and desktop files,
as well as dropping non-URL data (using KIO::paste internally)

This is a full-featured rewrite of KonqOperations::doDrop, with cleaner
code, a proper KIO::Job API, the popup menu is now async, and
the code is now fully unittested.

Change-Id: I9717f8a9e68a750696f2a54adfa828dead9a31f3
---
M autotests/CMakeLists.txt
A autotests/dropjobtest.cpp
M src/core/global.h
M src/core/job_error.cpp
M src/widgets/CMakeLists.txt
A src/widgets/dndpopupmenuplugin.cpp
A src/widgets/dndpopupmenuplugin.h
A src/widgets/dropjob.cpp
A src/widgets/dropjob.h
A src/widgets/kiodndpopupmenuplugin.desktop
M src/widgets/pastejob.cpp
M src/widgets/pastejob.h
A src/widgets/pastejob_p.h
13 files changed, 1,229 insertions(+), 47 deletions(-)


  git pull ssh://gerrit.vesnicky.cesnet.cz:29418/kio refs/changes/41/241/1

diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
index 38e01d7..c667bb9 100644
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@ -44,6 +44,7 @@
 
 ecm_add_tests(
  clipboardupdatertest.cpp
+ dropjobtest.cpp
  krununittest.cpp
  kdirlistertest.cpp
  kdirmodeltest.cpp
diff --git a/autotests/dropjobtest.cpp b/autotests/dropjobtest.cpp
new file mode 100644
index 0000000..e8f586f
--- /dev/null
+++ b/autotests/dropjobtest.cpp
@@ -0,0 +1,479 @@
+/* This file is part of the KDE project
+   Copyright (C) 2014 David Faure <faure at kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as published by
+   the Free Software Foundation; either version 2 of the License or ( at
+   your option ) version 3 or, at the discretion of KDE e.V. ( which shall
+   act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+
+#include <qtest.h>
+#include <QSignalSpy>
+#include <QDir>
+#include <QMenu>
+#include <QMimeData>
+#include <QStandardPaths>
+#include <QTemporaryDir>
+
+#include "kiotesthelper.h"
+
+#include <KIO/DropJob>
+#include <KIO/StatJob>
+#include <KIO/CopyJob>
+#include <KIO/DeleteJob>
+#include <KConfigGroup>
+#include <KDesktopFile>
+#include <KFileItemListProperties>
+
+Q_DECLARE_METATYPE(Qt::KeyboardModifiers)
+Q_DECLARE_METATYPE(Qt::DropAction)
+Q_DECLARE_METATYPE(Qt::DropActions)
+Q_DECLARE_METATYPE(KFileItemListProperties)
+
+#ifndef Q_OS_WIN
+void initLocale()
+{
+    setenv("LC_ALL", "en_US.utf-8", 1);
+}
+Q_CONSTRUCTOR_FUNCTION(initLocale)
+#endif
+
+class JobSpy : public QObject
+{
+    Q_OBJECT
+public:
+    JobSpy(KIO::Job *job)
+        : QObject(0),
+          m_spy(job, SIGNAL(result(KJob *))),
+          m_error(0)
+    {
+        connect(job, &KJob::result, this, [this](KJob* job) { m_error = job->error(); });
+    }
+    // like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...)
+    bool waitForResult() {
+        // implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller
+        if (m_spy.isEmpty()) {
+            QTest::qWait(0);
+        }
+        for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) {
+            QTest::qWait(50);
+        }
+        return !m_spy.isEmpty();
+    }
+    int error() const { return m_error; }
+
+private:
+    QSignalSpy m_spy;
+    int m_error;
+};
+
+class DropJobTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase()
+    {
+        QStandardPaths::setTestModeEnabled(true);
+        setenv("KIOSLAVE_ENABLE_TESTMODE", "1", 1); // ensure the ioslaves call QStandardPaths::setTestModeEnabled too
+
+        // To avoid a runtime dependency on klauncher
+        qputenv("KDE_FORK_SLAVES", "yes");
+
+        const QString trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QString::fromLatin1("/Trash");
+        QDir(trashDir).removeRecursively();
+
+        QVERIFY(m_tempDir.isValid());
+        QVERIFY(m_nonWritableTempDir.isValid());
+        QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner|QFile::ReadUser|QFile::ExeOwner|QFile::ExeUser));
+        m_srcDir = m_tempDir.path();
+
+        m_srcFile = m_srcDir + "/srcfile";
+        m_srcLink = m_srcDir + "/link";
+    }
+
+    void cleanupTestCase()
+    {
+        QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner|QFile::ReadUser|QFile::WriteOwner|QFile::WriteUser|QFile::ExeOwner|QFile::ExeUser));
+    }
+
+    // Before every test method, ensure the test file m_srcFile exists
+    void init()
+    {
+        if (QFile::exists(m_srcFile)) {
+            QVERIFY(QFileInfo(m_srcFile).isWritable());
+        } else {
+            QFile srcFile(m_srcFile);
+            QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
+            srcFile.write("Hello world\n");
+        }
+#ifndef Q_OS_WIN
+        if (!QFile::exists(m_srcLink)) {
+            QVERIFY(QFile(m_srcFile).link(m_srcLink));
+            QVERIFY(QFileInfo(m_srcLink).isSymLink());
+        }
+#endif
+        QVERIFY(QFileInfo(m_srcFile).isWritable());
+        m_mimeData.setUrls(QList<QUrl>() << QUrl::fromLocalFile(m_srcFile));
+    }
+
+    void shouldDropToDesktopFile()
+    {
+        // Given an executable application desktop file and a source file
+        const QString desktopPath = m_srcDir + "/target.desktop";
+        KDesktopFile desktopFile(desktopPath);
+        KConfigGroup desktopGroup = desktopFile.desktopGroup();
+        desktopGroup.writeEntry("Type", "Application");
+        desktopGroup.writeEntry("StartupNotify", "false");
+#ifdef Q_OS_WIN
+        desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest");
+#else
+        desktopGroup.writeEntry("Exec", "cp %f %d/dest");
+#endif
+        desktopFile.sync();
+        QFile file(desktopPath);
+        file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser);
+
+        // When dropping the source file onto the desktop file
+        QUrl destUrl = QUrl::fromLocalFile(desktopPath);
+        QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
+        KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        QSignalSpy spy(job, SIGNAL(itemCreated(QUrl)));
+
+        // Then the application is run with the source file as argument
+        // (in this example, it copies the source file to "dest")
+        QVERIFY2(job->exec(), qPrintable(job->errorString()));
+        QCOMPARE(spy.count(), 0);
+        const QString dest = m_srcDir + "/dest";
+        QTRY_VERIFY(QFile::exists(dest));
+
+        QVERIFY(QFile::remove(desktopPath));
+        QVERIFY(QFile::remove(dest));
+    }
+
+    void shouldDropToDirectory_data()
+    {
+        QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
+        QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
+        QTest::addColumn<QString>("srcFile");
+        QTest::addColumn<QString>("dest"); // empty for a temp dir
+        QTest::addColumn<int>("expectedError");
+        QTest::addColumn<bool>("shouldSourceStillExist");
+
+        QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString()
+            << 0 << true;
+        QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString()
+            << 0 << false;
+        QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString()
+            << 0 << true;
+        QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir
+            << int(KIO::ERR_DROP_ON_ITSELF) << true;
+        QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile
+            << int(KIO::ERR_ACCESS_DENIED) << true;
+        QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path()
+            << int(KIO::ERR_WRITE_ACCESS_DENIED) << true;
+    }
+
+    void shouldDropToDirectory()
+    {
+        QFETCH(Qt::KeyboardModifiers, modifiers);
+        QFETCH(Qt::DropAction, dropAction);
+        QFETCH(QString, srcFile);
+        QFETCH(QString, dest);
+        QFETCH(int, expectedError);
+        QFETCH(bool, shouldSourceStillExist);
+
+        // Given a directory and a source file
+        QTemporaryDir tempDestDir;
+        QVERIFY(tempDestDir.isValid());
+        if (dest.isEmpty()) {
+            dest = tempDestDir.path();
+        }
+
+        // When dropping the source file onto the directory
+        const QUrl destUrl = QUrl::fromLocalFile(dest);
+        m_mimeData.setUrls(QList<QUrl>() << QUrl::fromLocalFile(srcFile));
+        QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
+        KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        job->setUiDelegateExtension(0);
+        JobSpy jobSpy(job);
+        QSignalSpy spy(job, SIGNAL(itemCreated(QUrl)));
+
+        // Then the file is copied
+        QVERIFY(jobSpy.waitForResult());
+        QCOMPARE(jobSpy.error(), expectedError);
+        if (expectedError == 0) {
+            const QString destFile = dest + "/srcfile";
+            QCOMPARE(spy.count(), 1);
+            QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile));
+            QVERIFY(QFile::exists(destFile));
+            QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
+            if (dropAction == Qt::LinkAction) {
+                QVERIFY(QFileInfo(destFile).isSymLink());
+            }
+        }
+    }
+
+    void shouldDropToTrash_data()
+    {
+        QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
+        QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
+        QTest::addColumn<QString>("srcFile");
+
+        QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile;
+        QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile;
+        QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile;
+        QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile;
+#ifndef Q_OS_WIN
+        QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink;
+        QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink;
+        QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink;
+        QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink;
+#endif
+    }
+
+    void shouldDropToTrash()
+    {
+        // Given a source file
+        QFETCH(Qt::KeyboardModifiers, modifiers);
+        QFETCH(Qt::DropAction, dropAction);
+        QFETCH(QString, srcFile);
+        const bool isLink = QFileInfo(srcFile).isSymLink();
+
+        // When dropping it into the trash, with <modifiers> pressed
+        m_mimeData.setUrls(QList<QUrl>() << QUrl::fromLocalFile(srcFile));
+        QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
+        KIO::DropJob *job = KIO::drop(&dropEvent, QUrl("trash:/"), KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        QSignalSpy spy(job, SIGNAL(itemCreated(QUrl)));
+
+        // Then a confirmation dialog should appear
+        PredefinedAnswerJobUiDelegate extension;
+        extension.m_deleteResult = true;
+        job->setUiDelegateExtension(&extension);
+
+        // and the file should be moved to the trash, no matter what the modifiers are
+        QVERIFY2(job->exec(), qPrintable(job->errorString()));
+        QCOMPARE(extension.m_askDeleteCalled, 1);
+        QCOMPARE(spy.count(), 1);
+        const QUrl trashUrl = spy.at(0).at(0).value<QUrl>();
+        QCOMPARE(trashUrl.scheme(), QString("trash"));
+        KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
+        QVERIFY(statJob->exec());
+        if (isLink) {
+            QVERIFY(statJob->statResult().isLink());
+        }
+
+        // clean up
+        KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo);
+        QVERIFY2(delJob->exec(), qPrintable(delJob->errorString()));
+    }
+
+    void shouldDropFromTrash()
+    {
+        // Given a file in the trash
+        const QFile::Permissions origPerms = QFileInfo(m_srcFile).permissions();
+        QVERIFY(QFileInfo(m_srcFile).isWritable());
+        KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl("trash:/"));
+        QSignalSpy copyingDoneSpy(copyJob, SIGNAL(copyingDone(KIO::Job*, QUrl, QUrl, QDateTime, bool, bool)));
+        QVERIFY(copyJob->exec());
+        const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
+        QVERIFY(trashUrl.isValid());
+        QVERIFY(!QFile::exists(m_srcFile));
+
+        // When dropping the trashed file into a local dir, without modifiers
+        m_mimeData.setUrls(QList<QUrl>() << trashUrl);
+        QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
+        KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        QSignalSpy spy(job, SIGNAL(itemCreated(QUrl)));
+
+        // Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it.
+        QVERIFY2(job->exec(), qPrintable(job->errorString()));
+        QCOMPARE(spy.count(), 1);
+        QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile));
+        QVERIFY(QFile::exists(m_srcFile));
+        QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms));
+        QVERIFY(QFileInfo(m_srcFile).isWritable());
+        KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
+        QVERIFY(!statJob->exec());
+        QVERIFY(QFileInfo(m_srcFile).isWritable());
+    }
+
+    void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660
+    {
+        // Given some stuff in the trash
+        const QUrl trashUrl("trash:/");
+        KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl);
+        QVERIFY(copyJob->exec());
+        // and an empty destination directory
+        QTemporaryDir tempDestDir;
+        QVERIFY(tempDestDir.isValid());
+        const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
+
+        // When dropping a link / icon of the trash...
+        m_mimeData.setUrls(QList<QUrl>() << trashUrl);
+        QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
+        KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        QVERIFY2(job->exec(), qPrintable(job->errorString()));
+
+        // Then a full move shouldn't happen, just a link
+        const QStringList items = QDir(tempDestDir.path()).entryList();
+        QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(',')));
+        QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(',')));
+    }
+
+    void shouldDropToDirectoryWithPopup_data()
+    {
+        QTest::addColumn<QString>("dest"); // empty for a temp dir
+        QTest::addColumn<Qt::DropActions>("offeredActions");
+        QTest::addColumn<int>("triggerActionNumber");
+        QTest::addColumn<int>("expectedError");
+        QTest::addColumn<Qt::DropAction>("expectedDropAction");
+        QTest::addColumn<bool>("shouldSourceStillExist");
+
+        const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
+        const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction;
+        QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false;
+        QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true;
+        QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true;
+        QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true;
+        QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true;
+    }
+
+    void shouldDropToDirectoryWithPopup()
+    {
+        QFETCH(QString, dest);
+        QFETCH(Qt::DropActions, offeredActions);
+        QFETCH(int, triggerActionNumber);
+        QFETCH(int, expectedError);
+        QFETCH(Qt::DropAction, expectedDropAction);
+        QFETCH(bool, shouldSourceStillExist);
+
+        // Given a directory and a source file
+        QTemporaryDir tempDestDir;
+        QVERIFY(tempDestDir.isValid());
+        if (dest.isEmpty()) {
+            dest = tempDestDir.path();
+        }
+
+        // When dropping the source file onto the directory
+        QUrl destUrl = QUrl::fromLocalFile(dest);
+        QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
+        KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
+        job->setUiDelegate(0);
+        job->setUiDelegateExtension(0); // no rename dialog
+        JobSpy jobSpy(job);
+        qRegisterMetaType<KFileItemListProperties>();
+        QSignalSpy spyShow(job, SIGNAL(popupMenuAboutToShow(KFileItemListProperties)));
+        QVERIFY(spyShow.isValid());
+
+        // Then a popup should appear, with the expected available actions
+        QVERIFY(spyShow.wait());
+        QTRY_VERIFY(findPopup());
+        QMenu *popup = findPopup();
+        QCOMPARE(int(popupDropActions(popup)), int(offeredActions));
+
+        // And when selecting action number <triggerActionNumber>
+        QAction *action = popup->actions().at(triggerActionNumber);
+        QVERIFY(action);
+        QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction));
+        const QRect actionGeom = popup->actionGeometry(action);
+        QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
+
+        // Then the job should finish, and the chosen action should happen.
+        QVERIFY(jobSpy.waitForResult());
+        QCOMPARE(jobSpy.error(), expectedError);
+        if (expectedError == 0) {
+            const QString destFile = dest + "/srcfile";
+            QVERIFY(QFile::exists(destFile));
+            QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
+            if (expectedDropAction == Qt::LinkAction) {
+                QVERIFY(QFileInfo(destFile).isSymLink());
+            }
+        }
+        QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup
+    }
+
+    void shouldAddApplicationActionsToPopup()
+    {
+        // Given a directory and a source file
+        QTemporaryDir tempDestDir;
+        QVERIFY(tempDestDir.isValid());
+        const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
+
+        // When dropping the source file onto the directory
+        QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
+        KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
+        QAction appAction1("action1", this);
+        QAction appAction2("action2", this);
+        QList<QAction *> appActions; appActions << &appAction1 << &appAction2;
+        job->setUiDelegate(0);
+        job->setApplicationActions(appActions);
+        JobSpy jobSpy(job);
+
+        // Then a popup should appear, with the expected available actions
+        QTRY_VERIFY(findPopup());
+        QMenu *popup = findPopup();
+        const QList<QAction *> actions = popup->actions();
+        QVERIFY(actions.contains(&appAction1));
+        QVERIFY(actions.contains(&appAction2));
+        QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator());
+        QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator());
+
+        // And when selecting action appAction1
+        const QRect actionGeom = popup->actionGeometry(&appAction1);
+        QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
+
+        // Then the menu should hide and the job terminate (without doing any copying)
+        QVERIFY(jobSpy.waitForResult());
+        QCOMPARE(jobSpy.error(), 0);
+        const QString destFile = tempDestDir.path() + "/srcfile";
+        QVERIFY(!QFile::exists(destFile));
+    }
+
+private:
+    static QMenu *findPopup() {
+        Q_FOREACH (QWidget *widget, qApp->topLevelWidgets()) {
+            if (QMenu* menu = qobject_cast<QMenu*>(widget)) {
+                return menu;
+            }
+        }
+        return Q_NULLPTR;
+    }
+    static Qt::DropActions popupDropActions(QMenu *menu) {
+        Qt::DropActions actions;
+        Q_FOREACH (QAction *action, menu->actions()) {
+            const QVariant userData = action->data();
+            if (userData.isValid()) {
+                actions |= userData.value<Qt::DropAction>();
+            }
+        }
+        return actions;
+    }
+    QMimeData m_mimeData; // contains m_srcFile
+    QTemporaryDir m_tempDir;
+    QString m_srcDir;
+    QString m_srcFile;
+    QString m_srcLink;
+    QTemporaryDir m_nonWritableTempDir;
+};
+
+QTEST_MAIN(DropJobTest)
+
+#include "dropjobtest.moc"
+
diff --git a/src/core/global.h b/src/core/global.h
index a2426a3..d2c4b6a 100644
--- a/src/core/global.h
+++ b/src/core/global.h
@@ -241,7 +241,8 @@
     ERR_CANNOT_SEEK = KJob::UserDefinedError + 66,
     ERR_CANNOT_SETTIME = KJob::UserDefinedError + 67, // Emitted by setModificationTime
     ERR_CANNOT_CHOWN = KJob::UserDefinedError + 68,
-    ERR_POST_NO_SIZE = KJob::UserDefinedError + 69
+    ERR_POST_NO_SIZE = KJob::UserDefinedError + 69,
+    ERR_DROP_ON_ITSELF = KJob::UserDefinedError + 70 //< from KIO::DropJob, @since 5.6
 };
 
 /**
diff --git a/src/core/job_error.cpp b/src/core/job_error.cpp
index 6a5c35a..68fd77f 100644
--- a/src/core/job_error.cpp
+++ b/src/core/job_error.cpp
@@ -232,6 +232,9 @@
     case KIO::ERR_POST_NO_SIZE:
         result = i18n("The required content size information was not provided for a POST operation.");
         break;
+    case KIO::ERR_DROP_ON_ITSELF:
+        result = i18n("A file or folder cannot be dropped onto itself");
+        break;
     default:
         result = i18n("Unknown error code %1\n%2\nPlease send a full bug report at http://bugs.kde.org.",  errorCode,  errorText);
         break;
@@ -1018,6 +1021,13 @@
         solutions << i18n("Choose a different filename for the destination file.");
         break;
 
+    case KIO::ERR_DROP_ON_ITSELF:
+        errorName = i18n("File or Folder dropped onto itself");
+        description = i18n("The operation could not be completed because the "
+                           "source and destination file or folder are the same.");
+        solutions << i18n("Drop the item into a different file or folder.");
+        break;
+
     // We assume that the slave has all the details
     case KIO::ERR_SLAVE_DEFINED:
         errorName.clear();
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
index 8695895..4b3a288 100644
--- a/src/widgets/CMakeLists.txt
+++ b/src/widgets/CMakeLists.txt
@@ -44,6 +44,7 @@
   kshellcompletion.cpp
   kurlcompletion.cpp
   kurifilter.cpp
+  dropjob.cpp
   pastejob.cpp
   previewjob.cpp
   renamedialog.cpp
@@ -56,6 +57,7 @@
   kdirlister.cpp
   kdirmodel.cpp
   executablefileopendialog.cpp
+  dndpopupmenuplugin.cpp
 )
 if (WIN32)
   list(APPEND kiowidgets_SRCS
@@ -145,6 +147,8 @@
   SslUi
   ThumbSequenceCreator
   ThumbCreator
+  DropJob
+  DndPopupMenuPlugin
   PasteJob
   PreviewJob
   RenameDialog
@@ -179,6 +183,7 @@
   kfileitemactionplugin.desktop
   kpropertiesdialogplugin.desktop
   kurifilterplugin.desktop
+  kiodndpopupmenuplugin.desktop
    DESTINATION  ${SERVICETYPES_INSTALL_DIR} )
 
 include(ECMGeneratePriFile)
diff --git a/src/widgets/dndpopupmenuplugin.cpp b/src/widgets/dndpopupmenuplugin.cpp
new file mode 100644
index 0000000..5ee834f
--- /dev/null
+++ b/src/widgets/dndpopupmenuplugin.cpp
@@ -0,0 +1,30 @@
+/*  This file is part of the KDE project
+    Copyright 2009  Harald Hvaal <haraldhv at stud.ntnu.no>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) version 3.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "dndpopupmenuplugin.h"
+
+using namespace KIO;
+
+DndPopupMenuPlugin::DndPopupMenuPlugin(QObject* parent)
+    : QObject(parent)
+{
+}
+
+DndPopupMenuPlugin::~DndPopupMenuPlugin()
+{
+}
diff --git a/src/widgets/dndpopupmenuplugin.h b/src/widgets/dndpopupmenuplugin.h
new file mode 100644
index 0000000..6364295
--- /dev/null
+++ b/src/widgets/dndpopupmenuplugin.h
@@ -0,0 +1,67 @@
+/*  This file is part of the KDE project
+    Copyright 2009  Harald Hvaal <haraldhv at stud.ntnu.no>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) version 3.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+#ifndef _KIO_DNDPOPUPMENUPLUGIN_H_
+#define _KIO_DNDPOPUPMENUPLUGIN_H_
+
+#include "kiowidgets_export.h"
+#include <QObject>
+
+class QMenu;
+class KFileItemListProperties;
+class QUrl;
+class QAction;
+
+namespace KIO
+{
+/**
+ * Base class for drag and drop popup menus
+ *
+ * This can be used for adding dynamic menu items to the normal copy/move/link
+ * here menu appearing in dolphin/konqueror. In the setup method you may check
+ * the properties of the dropped files, and if applicable, append your own
+ * QAction that the user may trigger in the menu.
+ *
+ * @author Harald Hvaal <metellius at gmail.com>
+ * @since 5.6
+ */
+class KIOWIDGETS_EXPORT DndPopupMenuPlugin : public QObject
+{
+    Q_OBJECT
+public:
+
+    /**
+     * Constructor.
+     */
+    DndPopupMenuPlugin(QObject* parent);
+    virtual ~DndPopupMenuPlugin();
+
+    /**
+     * Implement the setup method in the plugin in order to create actions
+     * in the given actionCollection and add it to the menu using menu->addAction().
+     *
+     * @param popupMenuInfo all the information about the source URLs being dropped
+     * @param destination the URL to where the file(s) were dropped
+     * @return a QList with the QActions that will be plugged into the menu.
+     */
+    virtual QList<QAction *> setup(const KFileItemListProperties& popupMenuInfo,
+                                   const QUrl& destination) = 0;
+};
+
+}
+
+#endif
diff --git a/src/widgets/dropjob.cpp b/src/widgets/dropjob.cpp
new file mode 100644
index 0000000..7f33be4
--- /dev/null
+++ b/src/widgets/dropjob.cpp
@@ -0,0 +1,450 @@
+/* This file is part of the KDE libraries
+    Copyright (C) 2014 David Faure <faure at kde.org>
+
+    This library is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2 of the License or (at
+    your option) version 3 or, at the discretion of KDE e.V. (which shall
+    act as a proxy as in section 14 of the GPLv3), any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "dropjob.h"
+
+#include "job_p.h"
+#include "pastejob.h"
+#include "pastejob_p.h"
+#include "jobuidelegate.h"
+#include "jobuidelegateextension.h"
+
+#include <KConfigGroup>
+#include <KCoreDirLister>
+#include <KDesktopFile>
+#include <KIO/CopyJob>
+#include <KIO/DndPopupMenuPlugin>
+#include <KIO/FileUndoManager>
+#include <KFileItem>
+#include <KFileItemListProperties>
+#include <KJobWidgets>
+#include <KLocalizedString>
+#include <KMimeTypeTrader>
+#include <KProtocolManager>
+#include <KToolInvocation>
+#include <KUrlMimeData>
+
+#include <QApplication>
+#include <QDebug>
+#include <QDropEvent>
+#include <QFileInfo>
+#include <QMenu>
+#include <QMimeData>
+#include <QProcess>
+#include <QTimer>
+
+using namespace KIO;
+
+Q_DECLARE_METATYPE(Qt::DropAction);
+
+class KIO::DropJobPrivate : public KIO::JobPrivate
+{
+public:
+    DropJobPrivate(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags)
+        : JobPrivate(),
+          // Extract everything from the dropevent, since it will be deleted before the job starts
+          m_mimeData(dropEvent->mimeData()),
+          m_urls(KUrlMimeData::urlsFromMimeData(m_mimeData, KUrlMimeData::PreferLocalUrls, &m_metaData)),
+          m_dropAction(dropEvent->dropAction()),
+          m_globalPos(QCursor::pos()), // record mouse pos at time of drop
+          m_keyboardModifiers(dropEvent->keyboardModifiers()),
+          m_destUrl(destUrl),
+          m_destItem(KCoreDirLister::cachedItemForUrl(destUrl)),
+          m_flags(flags | KIO::HideProgressInfo)
+    {
+        // Check for the drop of a bookmark -> we want a Link action
+        if (m_mimeData->hasFormat("application/x-xbel")) {
+            m_keyboardModifiers |= Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier);
+            m_dropAction = Qt::LinkAction;
+        }
+        if (m_destItem.isNull() && m_destUrl.isLocalFile()) {
+            m_destItem = KFileItem(m_destUrl);
+        }
+    }
+
+    bool destIsDirectory() const {
+        if (!m_destItem.isNull()) {
+            return m_destItem.isDir();
+        }
+        // We support local dir, remote dir, local desktop file, local executable.
+        // So for remote URLs, we just assume they point to a directory, the user will get an error from KIO::copy if not.
+        return true;
+    }
+    void handleCopyToDirectory();
+    void handleDropToDesktopFile();
+    void handleDropToExecutable();
+    int determineDropAction();
+    void fillPopupMenu(QMenu *popup);
+    void addPluginActions(QMenu *popup, const KFileItemListProperties &itemProps);
+    void doCopyToDirectory();
+
+    const QMimeData *m_mimeData;
+    const QList<QUrl> m_urls;
+    QMap<QString, QString> m_metaData;
+    Qt::DropAction m_dropAction;
+    QPoint m_globalPos;
+    Qt::KeyboardModifiers m_keyboardModifiers;
+    QUrl m_destUrl;
+    KFileItem m_destItem; // null for remote URLs not found in the dirlister cache
+    const JobFlags m_flags;
+    QList<QAction *> m_appActions;
+    QList<QAction *> m_pluginActions;
+
+    Q_DECLARE_PUBLIC(DropJob)
+
+    void slotStart();
+    void slotCopyingDone(KIO::Job*, const QUrl &, const QUrl &to) {
+        emit q_func()->itemCreated(to);
+    }
+    void slotCopyingLinkDone(KIO::Job*, const QUrl &, const QString &, const QUrl &to) {
+        emit q_func()->itemCreated(to);
+    }
+    void slotTriggered(QAction *);
+
+    static inline DropJob *newJob(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags) {
+        DropJob *job = new DropJob(*new DropJobPrivate(dropEvent, destUrl, flags));
+        job->setUiDelegate(KIO::createDefaultJobUiDelegate());
+        if (!(flags & HideProgressInfo)) {
+            KIO::getJobTracker()->registerJob(job);
+        }
+        return job;
+    }
+
+};
+
+DropJob::DropJob(DropJobPrivate &dd)
+    : Job(dd)
+{
+    QTimer::singleShot(0, this, SLOT(slotStart()));
+}
+
+DropJob::~DropJob()
+{
+}
+
+void DropJobPrivate::slotStart()
+{
+    Q_Q(DropJob);
+    if (!m_urls.isEmpty()) {
+        if (destIsDirectory()) {
+            handleCopyToDirectory();
+        } else { // local file
+            const QString destFile = m_destUrl.toLocalFile();
+            if (KDesktopFile::isDesktopFile(destFile)) {
+                handleDropToDesktopFile();
+            } else if (QFileInfo(destFile).isExecutable()) {
+                handleDropToExecutable();
+            } else {
+                // should not happen, if KDirModel::flags is correct
+                q->setError(KIO::ERR_ACCESS_DENIED);
+                q->emitResult();
+            }
+        }
+        return;
+    }
+
+    // Dropping raw data
+    KIO::PasteJob *job = KIO::paste(m_mimeData, m_destUrl, KIO::HideProgressInfo);
+    job->d_func()->m_clipboard = false;
+    QObject::connect(job, &KIO::PasteJob::itemCreated, q, &KIO::DropJob::itemCreated);
+    q->addSubjob(job);
+}
+
+// Input: m_dropAction as set by Qt at the time of the drop event
+// Output: m_dropAction possibly modified
+// Returns a KIO error code, in case of error.
+int DropJobPrivate::determineDropAction()
+{
+    Q_Q(DropJob);
+
+    if (!KProtocolManager::supportsWriting(m_destUrl)) {
+        return KIO::ERR_CANNOT_WRITE;
+    }
+    if (!m_destItem.isNull() && !m_destItem.isWritable()) {
+        return KIO::ERR_WRITE_ACCESS_DENIED;
+    }
+
+    bool allItemsAreFromTrash = true;
+    bool containsTrashRoot = false;
+    foreach (const QUrl &url, m_urls) {
+        const bool local = url.isLocalFile();
+        //if (local && KDesktopFile::isDesktopFile(url.toLocalFile()))
+        //    isDesktopFile = true;
+        if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
+            if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
+                containsTrashRoot = true;
+            }
+        } else {
+            allItemsAreFromTrash = false;
+        }
+        if (url.matches(m_destUrl, QUrl::StripTrailingSlash)) {
+            return KIO::ERR_DROP_ON_ITSELF;
+        }
+    }
+
+    const bool trashing = m_destUrl.scheme() == QLatin1String("trash");
+    if (trashing) {
+        m_dropAction = Qt::MoveAction;
+        if (!q->uiDelegateExtension()->askDeleteConfirmation(m_urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) {
+            return KIO::ERR_USER_CANCELED;
+        }
+        return 0; // ok
+    }
+    if (containsTrashRoot) {
+        // Dropping a link to the trash: don't move the full contents, just make a link (#319660)
+        m_dropAction = Qt::LinkAction;
+        return 0; // ok
+    }
+    if (allItemsAreFromTrash) {
+        // No point in asking copy/move/link when using dragging from the trash, just move the file out.
+        m_dropAction = Qt::MoveAction;
+        return 0; // ok
+    }
+    if (m_keyboardModifiers & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier)) {
+        // Qt determined m_dropAction from the modifiers already
+        return 0; // ok
+    }
+
+    // We need to ask the user with a popup menu. Let the caller know.
+    return KIO::ERR_UNKNOWN;
+}
+
+void DropJobPrivate::fillPopupMenu(QMenu *popup)
+{
+    Q_Q(DropJob);
+
+    // Check what the source can do
+    // TODO: Determining the mimetype of the source URLs is difficult for remote URLs,
+    // we would need to KIO::stat each URL in turn, asynchronously....
+    KFileItemList fileItems;
+    foreach (const QUrl &url, m_urls) {
+        fileItems.append(KFileItem(url));
+    }
+    const KFileItemListProperties itemProps(fileItems);
+
+    emit q->popupMenuAboutToShow(fileItems);
+
+    const bool sReading = itemProps.supportsReading();
+    const bool sDeleting = itemProps.supportsDeleting();
+    const bool sMoving = itemProps.supportsMoving();
+
+    QString seq = QKeySequence(Qt::ShiftModifier).toString();
+    Q_ASSERT(seq.endsWith('+'));
+    seq.chop(1); // chop superfluous '+'
+    QAction* popupMoveAction = new QAction(i18n("&Move Here") + '\t' + seq, popup);
+    popupMoveAction->setIcon(QIcon::fromTheme("go-jump"));
+    popupMoveAction->setData(QVariant::fromValue(Qt::MoveAction));
+    seq = QKeySequence(Qt::ControlModifier).toString();
+    Q_ASSERT(seq.endsWith('+'));
+    seq.chop(1);
+    QAction* popupCopyAction = new QAction(i18n("&Copy Here") + '\t' + seq, popup);
+    popupCopyAction->setIcon(QIcon::fromTheme("edit-copy"));
+    popupCopyAction->setData(QVariant::fromValue(Qt::CopyAction));
+    seq = QKeySequence(Qt::ControlModifier + Qt::ShiftModifier).toString();
+    Q_ASSERT(seq.endsWith('+'));
+    seq.chop(1);
+    QAction* popupLinkAction = new QAction(i18n("&Link Here") + '\t' + seq, popup);
+    popupLinkAction->setIcon(QIcon::fromTheme("edit-link"));
+    popupLinkAction->setData(QVariant::fromValue(Qt::LinkAction));
+    QAction* popupCancelAction = new QAction(i18n("C&ancel") + '\t' + QKeySequence(Qt::Key_Escape).toString(), popup);
+    popupCancelAction->setIcon(QIcon::fromTheme("process-stop"));
+
+    if (sMoving || (sReading && sDeleting)) {
+        bool equalDestination = true;
+        foreach (const QUrl &src, m_urls) {
+            if (!m_destUrl.matches(src.adjusted(QUrl::RemoveFilename), QUrl::StripTrailingSlash)) {
+                equalDestination = false;
+                break;
+            }
+        }
+
+        if (!equalDestination) {
+            popup->addAction(popupMoveAction);
+        }
+    }
+
+    if (sReading) {
+        popup->addAction(popupCopyAction);
+    }
+
+    popup->addAction(popupLinkAction);
+
+    addPluginActions(popup, itemProps);
+
+    popup->addSeparator();
+    popup->addAction(popupCancelAction);
+}
+
+void DropJobPrivate::addPluginActions(QMenu *popup, const KFileItemListProperties &itemProps)
+{
+    Q_Q(DropJob);
+
+    const QString commonMimeType = itemProps.mimeType().isEmpty() ? QString("application/octet-stream") : itemProps.mimeType();
+    const KService::List plugin_offers = KMimeTypeTrader::self()->query(commonMimeType, "KIO/DndPopupMenu/Plugin", "exist Library");
+    foreach (const KService::Ptr &service, plugin_offers) {
+        //qDebug() << service->name() << service->library();
+        KIO::DndPopupMenuPlugin *plugin = service->createInstance<KIO::DndPopupMenuPlugin>(popup);
+        if (plugin) {
+            m_pluginActions += plugin->setup(itemProps, m_destUrl);
+        }
+    }
+
+    if (!m_appActions.isEmpty() || !m_pluginActions.isEmpty()) {
+        popup->addSeparator();
+        popup->addActions(m_appActions);
+        popup->addActions(m_pluginActions);
+    }
+
+}
+
+void DropJob::setApplicationActions(const QList<QAction *> &actions)
+{
+    Q_D(DropJob);
+    d->m_appActions = actions;
+}
+
+void DropJobPrivate::slotTriggered(QAction *action)
+{
+    Q_Q(DropJob);
+    if (m_appActions.contains(action) || m_pluginActions.contains(action)) {
+        q->emitResult();
+        return;
+    }
+    const QVariant data = action->data();
+    if (!data.canConvert<Qt::DropAction>()) {
+        q->setError(KIO::ERR_USER_CANCELED);
+        q->emitResult();
+        return;
+    }
+    m_dropAction = data.value<Qt::DropAction>();
+    doCopyToDirectory();
+}
+
+void DropJobPrivate::handleCopyToDirectory()
+{
+    Q_Q(DropJob);
+
+    if (int error = determineDropAction()) {
+        if (error == KIO::ERR_UNKNOWN) {
+            QMenu *menu = new QMenu(KJobWidgets::window(q));
+            QObject::connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
+            fillPopupMenu(menu);
+            // can't use new-style connect here
+            QObject::connect(menu, SIGNAL(triggered(QAction*)), q, SLOT(slotTriggered(QAction*)));
+            menu->popup(m_globalPos);
+        } else {
+            q->setError(error);
+            q->emitResult();
+        }
+    } else {
+        doCopyToDirectory();
+    }
+}
+
+void DropJobPrivate::doCopyToDirectory()
+{
+    Q_Q(DropJob);
+    KIO::CopyJob * job = 0;
+    switch (m_dropAction) {
+    case Qt::MoveAction:
+        job = KIO::move(m_urls, m_destUrl, m_flags);
+        KIO::FileUndoManager::self()->recordJob(
+            m_destUrl.scheme() == QLatin1String("trash") ? KIO::FileUndoManager::Trash : KIO::FileUndoManager::Move,
+            m_urls, m_destUrl, job);
+        break;
+    case Qt::CopyAction:
+        job = KIO::copy(m_urls, m_destUrl, m_flags);
+        KIO::FileUndoManager::self()->recordCopyJob(job);
+        break;
+    case Qt::LinkAction:
+        job = KIO::link(m_urls, m_destUrl, m_flags);
+        KIO::FileUndoManager::self()->recordCopyJob(job);
+        break;
+    default:
+        qWarning() << "Unknown drop action" << (int)m_dropAction;
+        q->setError(KIO::ERR_UNSUPPORTED_ACTION);
+        q->emitResult();
+        return;
+    }
+    Q_ASSERT(job);
+    job->setMetaData(m_metaData);
+    QObject::connect(job, SIGNAL(copyingDone(KIO::Job*, QUrl, QUrl, QDateTime, bool, bool)), q, SLOT(slotCopyingDone(KIO::Job*, QUrl, QUrl)));
+    QObject::connect(job, SIGNAL(copyingLinkDone(KIO::Job*, QUrl, QString, QUrl)), q, SLOT(slotCopyingLinkDone(KIO::Job*, QUrl, QString, QUrl)));
+    q->addSubjob(job);
+}
+
+void DropJobPrivate::handleDropToDesktopFile()
+{
+    Q_Q(DropJob);
+    const QString destFile = m_destUrl.toLocalFile();
+    const KDesktopFile desktopFile(destFile);
+    const KConfigGroup desktopGroup = desktopFile.desktopGroup();
+    if (desktopFile.hasApplicationType()) {
+        // Drop to application -> start app with urls as argument
+        QString error;
+        if (KToolInvocation::startServiceByDesktopPath(destFile, QUrl::toStringList(m_urls), &error) > 0) {
+            q->setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
+            q->setErrorText(error);
+        }
+        q->emitResult();
+    } else if (desktopFile.hasLinkType() && desktopGroup.hasKey("URL")) {
+        // Drop to link -> adjust destination directory
+        m_destUrl = QUrl::fromUserInput(desktopGroup.readPathEntry("URL", QString()));
+        handleCopyToDirectory();
+    } else {
+        if (desktopFile.hasDeviceType()) {
+            qWarning() << "Not re-implemented; please email kde-frameworks-devel at kde.org if you need this.";
+            // take code from libkonq's old konq_operations.cpp
+            // for now, fallback
+        }
+        // Some other kind of .desktop file (service, servicetype...)
+        q->setError(KIO::ERR_UNSUPPORTED_ACTION);
+        q->emitResult();
+    }
+}
+
+void DropJobPrivate::handleDropToExecutable()
+{
+    // Launch executable for each of the files
+    QStringList args;
+    Q_FOREACH(const QUrl &url, m_urls) {
+        args << url.toLocalFile(); // assume local files
+    }
+    //qDebug() << "starting" << m_destUrl.toLocalFile() << "with" << lst.count() << "arguments";
+    QProcess::startDetached(m_destUrl.toLocalFile(), args);
+}
+
+void DropJob::slotResult(KJob *job)
+{
+    Q_D(DropJob);
+    if (job->error()) {
+        KIO::Job::slotResult(job); // will set the error and emit result(this)
+        return;
+    }
+    removeSubjob(job);
+    emitResult();
+}
+
+DropJob * KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags)
+{
+    return DropJobPrivate::newJob(dropEvent, destUrl, flags);
+}
+
+#include "moc_dropjob.cpp"
diff --git a/src/widgets/dropjob.h b/src/widgets/dropjob.h
new file mode 100644
index 0000000..db67a90
--- /dev/null
+++ b/src/widgets/dropjob.h
@@ -0,0 +1,114 @@
+/* This file is part of the KDE libraries
+    Copyright (C) 2014 David Faure <faure at kde.org>
+
+    This library is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2 of the License or ( at
+    your option ) version 3 or, at the discretion of KDE e.V. ( which shall
+    act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DROPJOB_H
+#define DROPJOB_H
+
+#include <QUrl>
+
+#include "kiowidgets_export.h"
+#include <kio/job_base.h>
+
+class QAction;
+class QDropEvent;
+class QMimeData;
+class KFileItemListProperties;
+
+namespace KIO
+{
+
+class DropJobPrivate;
+
+/**
+ * A KIO job that handles dropping into a file-manager-like view.
+ * @see KIO::drop
+ * @since 5.6
+ */
+class KIOWIDGETS_EXPORT DropJob : public Job
+{
+    Q_OBJECT
+
+public:
+    virtual ~DropJob();
+
+    /**
+     * Allows the application to set additional actions in the drop popup menu.
+     * For instance, the application handling the desktop might want to add
+     * "set as wallpaper" if the dropped url is an image file.
+     * This can be called upfront, or for convenience, when popupMenuAboutToShow is emitted.
+     */
+    void setApplicationActions(const QList<QAction *> &actions);
+
+Q_SIGNALS:
+    /**
+     * Signals that a file or directory was created.
+     */
+    void itemCreated(const QUrl &url);
+
+    /**
+     * Signals that the popup menu is about to be shown.
+     * Applications can use the information provided about the dropped URLs
+     * (e.g. the mimetype) to decide whether to call setApplicationActions.
+     * @param itemProps properties of the dropped items
+     */
+    void popupMenuAboutToShow(const KFileItemListProperties &itemProps);
+
+protected Q_SLOTS:
+    void slotResult(KJob *job) Q_DECL_OVERRIDE;
+
+protected:
+    DropJob(DropJobPrivate &dd);
+
+private:
+    Q_DECLARE_PRIVATE(DropJob)
+    Q_PRIVATE_SLOT(d_func(), void slotStart());
+    Q_PRIVATE_SLOT(d_func(), void slotCopyingDone(KIO::Job*, const QUrl &, const QUrl &to));
+    Q_PRIVATE_SLOT(d_func(), void slotCopyingLinkDone(KIO::Job*, const QUrl &, const QString &, const QUrl &to));
+    Q_PRIVATE_SLOT(d_func(), void slotTriggered(QAction*));
+};
+
+/**
+ * Drops the clipboard contents.
+ *
+ * If the mime data contains URLs, a popup appears to choose between
+ *  Move, Copy, Link and Cancel
+ * which is then implemented by the job, using KIO::move, KIO::copy or KIO::link
+ * Additional actions provided by the application or by plugins can be shown in the popup.
+ *
+ * If the mime data contains other data than URLs, it is saved into a file after asking
+ * the user to choose a filename and the preferred data format.
+ *
+ * This job takes care of recording the subjob in the FileUndoManager, and emits
+ * itemCreated for every file or directory being created, so that the view can select
+ * these items.
+ *
+ * @param dropEvent the drop event, from which the job will extract mimeData, dropAction, etc.
+         The application should take care of calling dropEvent->acceptProposedAction().
+ * @param destUrl The URL of the target file or directory
+ * @param flags passed to the sub job
+ *
+ * @return A pointer to the job handling the operation.
+ * @since 5.4
+ */
+KIOCORE_EXPORT DropJob *drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags = DefaultFlags);
+
+}
+
+#endif
diff --git a/src/widgets/kiodndpopupmenuplugin.desktop b/src/widgets/kiodndpopupmenuplugin.desktop
new file mode 100644
index 0000000..5d4d277
--- /dev/null
+++ b/src/widgets/kiodndpopupmenuplugin.desktop
@@ -0,0 +1,4 @@
+[Desktop Entry]
+Type=ServiceType
+X-KDE-ServiceType=KIO/DndPopupMenu/Plugin
+Comment=Plugin for the KIO Drag-and-drop Popup Menu
diff --git a/src/widgets/pastejob.cpp b/src/widgets/pastejob.cpp
index 7ad93da..3f08d61 100644
--- a/src/widgets/pastejob.cpp
+++ b/src/widgets/pastejob.cpp
@@ -18,8 +18,7 @@
 */
 
 #include "pastejob.h"
-
-#include "job_p.h"
+#include "pastejob_p.h"
 
 #include "paste.h"
 
@@ -39,41 +38,6 @@
 extern KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl,
                                    const QString &dialogText, QWidget *widget,
                                    bool clipboard);
-
-class KIO::PasteJobPrivate : public KIO::JobPrivate
-{
-public:
-    PasteJobPrivate(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags, bool clipboard)
-        : JobPrivate(),
-        m_mimeData(mimeData),
-        m_destDir(destDir),
-        m_flags(flags),
-        m_clipboard(clipboard)
-    {
-    }
-
-    const QMimeData *m_mimeData;
-    QUrl m_destDir;
-    JobFlags m_flags;
-    bool m_clipboard;
-
-    Q_DECLARE_PUBLIC(PasteJob)
-
-    void slotStart();
-    void slotCopyingDone(KIO::Job*, const QUrl &, const QUrl &to) { emit q_func()->itemCreated(to); }
-    void slotCopyingLinkDone(KIO::Job*, const QUrl &, const QString &, const QUrl &to) { emit q_func()->itemCreated(to); }
-
-    static inline PasteJob *newJob(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags, bool clipboard)
-    {
-        PasteJob *job = new PasteJob(*new PasteJobPrivate(mimeData, destDir, flags, clipboard));
-        job->setUiDelegate(KIO::createDefaultJobUiDelegate());
-        if (!(flags & HideProgressInfo)) {
-            KIO::getJobTracker()->registerJob(job);
-        }
-        return job;
-    }
-
-};
 
 PasteJob::PasteJob(PasteJobPrivate &dd)
     : Job(dd)
@@ -137,15 +101,7 @@
 
 PasteJob * KIO::paste(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags)
 {
-    return PasteJobPrivate::newJob(mimeData, destDir, flags, true);
+    return PasteJobPrivate::newJob(mimeData, destDir, flags);
 }
-
-/*
-   To be called from the drop job directly.
-PasteJob * KIO::drop(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags)
-{
-    return PasteJobPrivate::newJob(mimeData, destDir, flags, false);
-}
-*/
 
 #include "moc_pastejob.cpp"
diff --git a/src/widgets/pastejob.h b/src/widgets/pastejob.h
index 026e152..2d08929 100644
--- a/src/widgets/pastejob.h
+++ b/src/widgets/pastejob.h
@@ -31,6 +31,7 @@
 {
 
 class PasteJobPrivate;
+class DropJobPrivate;
 /**
  * A KIO job that handles pasting the clipboard contents.
  *
@@ -61,6 +62,7 @@
     PasteJob(PasteJobPrivate &dd);
 
 private:
+    friend class KIO::DropJobPrivate;
     Q_DECLARE_PRIVATE(PasteJob)
     Q_PRIVATE_SLOT(d_func(), void slotStart());
     Q_PRIVATE_SLOT(d_func(), void slotCopyingDone(KIO::Job*, const QUrl &, const QUrl &to));
diff --git a/src/widgets/pastejob_p.h b/src/widgets/pastejob_p.h
new file mode 100644
index 0000000..41e63fa
--- /dev/null
+++ b/src/widgets/pastejob_p.h
@@ -0,0 +1,63 @@
+/* This file is part of the KDE libraries
+    Copyright (C) 2014 David Faure <faure at kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef PASTEJOB_P_H
+#define PASTEJOB_P_H
+
+#include <job_p.h>
+
+namespace KIO { class DropJobPrivate; }
+
+class KIO::PasteJobPrivate : public KIO::JobPrivate
+{
+public:
+    PasteJobPrivate(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags)
+        : JobPrivate(),
+        m_mimeData(mimeData),
+        m_destDir(destDir),
+        m_flags(flags),
+        m_clipboard(true) // set to false by DropJob
+    {
+    }
+
+    friend class KIO::DropJobPrivate;
+
+    const QMimeData *m_mimeData;
+    QUrl m_destDir;
+    JobFlags m_flags;
+    bool m_clipboard;
+
+    Q_DECLARE_PUBLIC(PasteJob)
+
+    void slotStart();
+    void slotCopyingDone(KIO::Job*, const QUrl &, const QUrl &to) { emit q_func()->itemCreated(to); }
+    void slotCopyingLinkDone(KIO::Job*, const QUrl &, const QString &, const QUrl &to) { emit q_func()->itemCreated(to); }
+
+    static inline PasteJob *newJob(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags)
+    {
+        PasteJob *job = new PasteJob(*new PasteJobPrivate(mimeData, destDir, flags));
+        job->setUiDelegate(KIO::createDefaultJobUiDelegate());
+        if (!(flags & HideProgressInfo)) {
+            KIO::getJobTracker()->registerJob(job);
+        }
+        return job;
+    }
+};
+
+#endif

-- 
To view, visit https://gerrit.vesnicky.cesnet.cz/r/241
To unsubscribe, visit https://gerrit.vesnicky.cesnet.cz/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I9717f8a9e68a750696f2a54adfa828dead9a31f3
Gerrit-PatchSet: 1
Gerrit-Project: kio
Gerrit-Branch: master
Gerrit-Owner: David Faure <faure at kde.org>
Gerrit-Reviewer: Sysadmin Testing Account <null at kde.org>


More information about the Kde-frameworks-devel mailing list