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 <fa...@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 <haral...@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 <haral...@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 <metell...@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 <fa...@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@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 <fa...@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 <fa...@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 <fa...@kde.org> Gerrit-Reviewer: Sysadmin Testing Account <n...@kde.org> _______________________________________________ Kde-frameworks-devel mailing list Kde-frameworks-devel@kde.org https://mail.kde.org/mailman/listinfo/kde-frameworks-devel