diff --git a/autotests/fileundomanagertest.cpp b/autotests/fileundomanagertest.cpp index 8ae9269e..85a45747 100644 --- a/autotests/fileundomanagertest.cpp +++ b/autotests/fileundomanagertest.cpp @@ -1,688 +1,724 @@ /* This file is part of KDE Copyright (c) 2006, 2008 David Faure 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. */ #include #include "fileundomanagertest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #include #endif #include #include #include QTEST_MAIN(FileUndoManagerTest) using namespace KIO; static QString homeTmpDir() { return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QDir::separator(); } static QString destDir() { return homeTmpDir() + "destdir/"; } static QString srcFile() { return homeTmpDir() + "testfile"; } static QString destFile() { return destDir() + "testfile"; } #ifndef Q_OS_WIN static QString srcLink() { return homeTmpDir() + "symlink"; } static QString destLink() { return destDir() + "symlink"; } #endif static QString srcSubDir() { return homeTmpDir() + "subdir"; } static QString destSubDir() { return destDir() + "subdir"; } static QList sourceList() { QList lst; lst << QUrl::fromLocalFile(srcFile()); #ifndef Q_OS_WIN lst << QUrl::fromLocalFile(srcLink()); #endif return lst; } static void createTestFile(const QString &path, const char *contents) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { qFatal("Couldn't create %s", qPrintable(path)); } f.write(QByteArray(contents)); f.close(); } static void createTestSymlink(const QString &path) { // Create symlink if it doesn't exist yet QT_STATBUF buf; if (QT_LSTAT(QFile::encodeName(path).constData(), &buf) != 0) { bool ok = KIOPrivate::createSymlink(QStringLiteral("/IDontExist"), path); // broken symlink if (!ok) { qFatal("couldn't create symlink: %s", strerror(errno)); } QVERIFY(QT_LSTAT(QFile::encodeName(path).constData(), &buf) == 0); QVERIFY((buf.st_mode & QT_STAT_MASK) == QT_STAT_LNK); } else { QVERIFY((buf.st_mode & QT_STAT_MASK) == QT_STAT_LNK); } qDebug("symlink %s created", qPrintable(path)); QVERIFY(QFileInfo(path).isSymLink()); } static void checkTestDirectory(const QString &path) { QVERIFY(QFileInfo(path).isDir()); QVERIFY(QFileInfo(path + "/fileindir").isFile()); #ifndef Q_OS_WIN QVERIFY(QFileInfo(path + "/testlink").isSymLink()); #endif QVERIFY(QFileInfo(path + "/dirindir").isDir()); QVERIFY(QFileInfo(path + "/dirindir/nested").isFile()); } static void createTestDirectory(const QString &path) { QDir dir; bool ok = dir.mkpath(path); if (!ok) { qFatal("couldn't create %s", qPrintable(path)); } createTestFile(path + "/fileindir", "File in dir"); #ifndef Q_OS_WIN createTestSymlink(path + "/testlink"); #endif ok = dir.mkdir(path + "/dirindir"); if (!ok) { qFatal("couldn't create %s", qPrintable(path)); } createTestFile(path + "/dirindir/nested", "Nested"); checkTestDirectory(path); } class TestUiInterface : public FileUndoManager::UiInterface { public: TestUiInterface() : FileUndoManager::UiInterface(), m_nextReplyToConfirmDeletion(true) { setShowProgressInfo(false); } void jobError(KIO::Job *job) Q_DECL_OVERRIDE { qFatal("%s", qPrintable(job->errorString())); } bool copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) Q_DECL_OVERRIDE { Q_UNUSED(src); m_dest = dest; Q_UNUSED(srcTime); Q_UNUSED(destTime); return true; } bool confirmDeletion(const QList &files) Q_DECL_OVERRIDE { m_files = files; return m_nextReplyToConfirmDeletion; } void setNextReplyToConfirmDeletion(bool b) { m_nextReplyToConfirmDeletion = b; } QList files() const { return m_files; } QUrl dest() const { return m_dest; } void clear() { m_dest = QUrl(); m_files.clear(); } private: bool m_nextReplyToConfirmDeletion; QUrl m_dest; QList m_files; }; void FileUndoManagerTest::initTestCase() { qDebug("initTestCase"); QStandardPaths::enableTestMode(true); // Get kio_trash to share our environment so that it writes trashrc to the right kdehome qputenv("KDE_FORK_SLAVES", "yes"); qputenv("KIOSLAVE_ENABLE_TESTMODE", "1"); // Start with a clean base dir cleanupTestCase(); if (!QFile::exists(homeTmpDir())) { bool ok = QDir().mkpath(homeTmpDir()); if (!ok) { qFatal("Couldn't create %s", qPrintable(homeTmpDir())); } } createTestFile(srcFile(), "Hello world"); #ifndef Q_OS_WIN createTestSymlink(srcLink()); #endif createTestDirectory(srcSubDir()); QDir().mkpath(destDir()); QVERIFY(QFileInfo(destDir()).isDir()); QVERIFY(!FileUndoManager::self()->undoAvailable()); m_uiInterface = new TestUiInterface; // owned by FileUndoManager FileUndoManager::self()->setUiInterface(m_uiInterface); } void FileUndoManagerTest::cleanupTestCase() { KIO::Job *job = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo); job->exec(); } void FileUndoManagerTest::doUndo() { QEventLoop eventLoop; bool ok = connect(FileUndoManager::self(), SIGNAL(undoJobFinished()), &eventLoop, SLOT(quit())); QVERIFY(ok); FileUndoManager::self()->undo(); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); // wait for undo job to finish } void FileUndoManagerTest::testCopyFiles() { qDebug(); // Initially inspired from JobTest::copyFileToSamePartition() const QString destdir = destDir(); QList lst = sourceList(); const QUrl d = QUrl::fromLocalFile(destdir); KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); QSignalSpy spyUndoAvailable(FileUndoManager::self(), SIGNAL(undoAvailable(bool))); QVERIFY(spyUndoAvailable.isValid()); QSignalSpy spyTextChanged(FileUndoManager::self(), SIGNAL(undoTextChanged(QString))); QVERIFY(spyTextChanged.isValid()); bool ok = job->exec(); QVERIFY(ok); QVERIFY(QFile::exists(destFile())); #ifndef Q_OS_WIN // Don't use QFile::exists, it's a broken symlink... QVERIFY(QFileInfo(destLink()).isSymLink()); #endif // might have to wait for dbus signal here... but this is currently disabled. //QTest::qWait( 20 ); QVERIFY(FileUndoManager::self()->undoAvailable()); QCOMPARE(spyUndoAvailable.count(), 1); QCOMPARE(spyTextChanged.count(), 1); m_uiInterface->clear(); m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm FileUndoManager::self()->undo(); QCOMPARE(m_uiInterface->files().count(), 1); // confirmDeletion was called QCOMPARE(m_uiInterface->files()[0].toString(), QUrl::fromLocalFile(destFile()).toString()); QVERIFY(QFile::exists(destFile())); // nothing happened yet // OK, now do it m_uiInterface->clear(); m_uiInterface->setNextReplyToConfirmDeletion(true); doUndo(); QVERIFY(!FileUndoManager::self()->undoAvailable()); QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well QCOMPARE(spyTextChanged.count(), 2); QCOMPARE(m_uiInterface->files().count(), 1); // confirmDeletion was called QCOMPARE(m_uiInterface->files()[0].toString(), QUrl::fromLocalFile(destFile()).toString()); // Check that undo worked QVERIFY(!QFile::exists(destFile())); #ifndef Q_OS_WIN QVERIFY(!QFile::exists(destLink())); QVERIFY(!QFileInfo(destLink()).isSymLink()); #endif } void FileUndoManagerTest::testMoveFiles() { qDebug(); const QString destdir = destDir(); QList lst = sourceList(); const QUrl d = QUrl::fromLocalFile(destdir); KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(srcFile())); // the source moved QVERIFY(QFile::exists(destFile())); #ifndef Q_OS_WIN QVERIFY(!QFileInfo(srcLink()).isSymLink()); // Don't use QFile::exists, it's a broken symlink... QVERIFY(QFileInfo(destLink()).isSymLink()); #endif doUndo(); QVERIFY(QFile::exists(srcFile())); // the source is back QVERIFY(!QFile::exists(destFile())); #ifndef Q_OS_WIN QVERIFY(QFileInfo(srcLink()).isSymLink()); QVERIFY(!QFileInfo(destLink()).isSymLink()); #endif } // Testing for overwrite isn't possible, because non-interactive jobs never overwrite. // And nothing different happens anyway, the dest is removed... #if 0 void FileUndoManagerTest::testCopyFilesOverwrite() { qDebug(); // Create a different file in the destdir createTestFile(destFile(), "An old file already in the destdir"); testCopyFiles(); } #endif void FileUndoManagerTest::testCopyDirectory() { const QString destdir = destDir(); QList lst; lst << QUrl::fromLocalFile(srcSubDir()); const QUrl d = QUrl::fromLocalFile(destdir); KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); bool ok = job->exec(); QVERIFY(ok); checkTestDirectory(srcSubDir()); // src untouched checkTestDirectory(destSubDir()); doUndo(); checkTestDirectory(srcSubDir()); QVERIFY(!QFile::exists(destSubDir())); } void FileUndoManagerTest::testMoveDirectory() { const QString destdir = destDir(); QList lst; lst << QUrl::fromLocalFile(srcSubDir()); const QUrl d = QUrl::fromLocalFile(destdir); KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(srcSubDir())); checkTestDirectory(destSubDir()); doUndo(); checkTestDirectory(srcSubDir()); QVERIFY(!QFile::exists(destSubDir())); } void FileUndoManagerTest::testRenameFile() { const QUrl oldUrl = QUrl::fromLocalFile(srcFile()); const QUrl newUrl = QUrl::fromLocalFile(srcFile() + ".new"); QList lst; lst.append(oldUrl); QSignalSpy spyUndoAvailable(FileUndoManager::self(), SIGNAL(undoAvailable(bool))); QVERIFY(spyUndoAvailable.isValid()); KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(srcFile())); QVERIFY(QFileInfo(newUrl.toLocalFile()).isFile()); QCOMPARE(spyUndoAvailable.count(), 1); doUndo(); QVERIFY(QFile::exists(srcFile())); QVERIFY(!QFileInfo(newUrl.toLocalFile()).isFile()); } void FileUndoManagerTest::testRenameDir() { const QUrl oldUrl = QUrl::fromLocalFile(srcSubDir()); const QUrl newUrl = QUrl::fromLocalFile(srcSubDir() + ".new"); QList lst; lst.append(oldUrl); KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(srcSubDir())); QVERIFY(QFileInfo(newUrl.toLocalFile()).isDir()); doUndo(); QVERIFY(QFile::exists(srcSubDir())); QVERIFY(!QFileInfo(newUrl.toLocalFile()).isDir()); } void FileUndoManagerTest::testCreateSymlink() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows for lack of proper symlink support"); #endif const QUrl link = QUrl::fromLocalFile(homeTmpDir() + "newlink"); const QString path = link.toLocalFile(); QVERIFY(!QFile::exists(path)); const QUrl target = QUrl::fromLocalFile(homeTmpDir() + "linktarget"); const QString targetPath = target.toLocalFile(); createTestFile(targetPath, "Link's Target"); QVERIFY(QFile::exists(targetPath)); KIO::CopyJob *job = KIO::link(target, link); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(QFile::exists(path)); QVERIFY(QFileInfo(path).isSymLink()); // For undoing symlinks no confirmation is required. We delete it straight away. doUndo(); QVERIFY(!QFile::exists(path)); } void FileUndoManagerTest::testCreateDir() { const QUrl url = QUrl::fromLocalFile(srcSubDir() + ".mkdir"); const QString path = url.toLocalFile(); QVERIFY(!QFile::exists(path)); KIO::SimpleJob *job = KIO::mkdir(url); job->setUiDelegate(nullptr); FileUndoManager::self()->recordJob(FileUndoManager::Mkdir, QList(), url, job); bool ok = job->exec(); QVERIFY(ok); QVERIFY(QFile::exists(path)); QVERIFY(QFileInfo(path).isDir()); m_uiInterface->clear(); m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm FileUndoManager::self()->undo(); QCOMPARE(m_uiInterface->files().count(), 1); // confirmDeletion was called QCOMPARE(m_uiInterface->files()[0].toString(), url.toString()); QVERIFY(QFile::exists(path)); // nothing happened yet // OK, now do it m_uiInterface->clear(); m_uiInterface->setNextReplyToConfirmDeletion(true); doUndo(); QVERIFY(!QFile::exists(path)); } void FileUndoManagerTest::testMkpath() { const QString parent = srcSubDir() + "mkpath"; const QString path = parent + "/subdir"; QVERIFY(!QFile::exists(path)); const QUrl url = QUrl::fromLocalFile(path); KIO::Job *job = KIO::mkpath(url); job->setUiDelegate(nullptr); FileUndoManager::self()->recordJob(FileUndoManager::Mkpath, QList(), url, job); QVERIFY(job->exec()); QVERIFY(QFileInfo(path).isDir()); m_uiInterface->clear(); m_uiInterface->setNextReplyToConfirmDeletion(true); doUndo(); QVERIFY(!FileUndoManager::self()->undoAvailable()); QCOMPARE(m_uiInterface->files().count(), 2); // confirmDeletion was called QCOMPARE(m_uiInterface->files()[0].toLocalFile(), path); QCOMPARE(m_uiInterface->files()[1].toLocalFile(), parent); QVERIFY(!QFile::exists(path)); } void FileUndoManagerTest::testTrashFiles() { if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) { QSKIP("kio_trash not installed"); } // Trash it all at once: the file, the symlink, the subdir. QList lst = sourceList(); lst.append(QUrl::fromLocalFile(srcSubDir())); KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordJob(FileUndoManager::Trash, lst, QUrl(QStringLiteral("trash:/")), job); bool ok = job->exec(); QVERIFY(ok); // Check that things got removed QVERIFY(!QFile::exists(srcFile())); #ifndef Q_OS_WIN QVERIFY(!QFileInfo(srcLink()).isSymLink()); #endif QVERIFY(!QFile::exists(srcSubDir())); // check trash? // Let's just check that it's not empty. kio_trash has its own unit tests anyway. KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); QVERIFY(cfg.hasGroup("Status")); QCOMPARE(cfg.group("Status").readEntry("Empty", true), false); doUndo(); QVERIFY(QFile::exists(srcFile())); #ifndef Q_OS_WIN QVERIFY(QFileInfo(srcLink()).isSymLink()); #endif QVERIFY(QFile::exists(srcSubDir())); // We can't check that the trash is empty; other partitions might have their own trash } void FileUndoManagerTest::testRestoreTrashedFiles() { if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) { QSKIP("kio_trash not installed"); } // Trash it all at once: the file, the symlink, the subdir. const QFile::Permissions origPerms = QFileInfo(srcFile()).permissions(); QList lst = sourceList(); lst.append(QUrl::fromLocalFile(srcSubDir())); KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(job->exec()); const QMap metaData = job->metaData(); QList trashUrls; foreach (const QUrl &src, lst) { QMap::ConstIterator it = metaData.find("trashURL-" + src.path()); QVERIFY(it != metaData.constEnd()); trashUrls.append(QUrl(it.value())); } qDebug() << trashUrls; // Restore from trash KIO::RestoreJob *restoreJob = KIO::restoreFromTrash(trashUrls, KIO::HideProgressInfo); restoreJob->setUiDelegate(nullptr); QVERIFY(restoreJob->exec()); QVERIFY(QFile::exists(srcFile())); QCOMPARE(QFileInfo(srcFile()).permissions(), origPerms); #ifndef Q_OS_WIN QVERIFY(QFileInfo(srcLink()).isSymLink()); #endif QVERIFY(QFile::exists(srcSubDir())); // TODO support for RestoreJob in FileUndoManager !!! } static void setTimeStamp(const QString &path) { #ifdef Q_OS_UNIX // Put timestamp in the past so that we can check that the // copy actually preserves it. struct timeval tp; gettimeofday(&tp, nullptr); struct utimbuf utbuf; utbuf.actime = tp.tv_sec + 30; // 30 seconds in the future utbuf.modtime = tp.tv_sec + 60; // 60 second in the future utime(QFile::encodeName(path).constData(), &utbuf); qDebug("Time changed for %s", qPrintable(path)); #endif } void FileUndoManagerTest::testModifyFileBeforeUndo() { // based on testCopyDirectory (so that we check that it works for files in subdirs too) const QString destdir = destDir(); QList lst; lst << QUrl::fromLocalFile(srcSubDir()); const QUrl d = QUrl::fromLocalFile(destdir); KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); FileUndoManager::self()->recordCopyJob(job); bool ok = job->exec(); QVERIFY(ok); checkTestDirectory(srcSubDir()); // src untouched checkTestDirectory(destSubDir()); const QString destFile = destSubDir() + "/fileindir"; setTimeStamp(destFile); // simulate a modification of the file doUndo(); // Check that TestUiInterface::copiedFileWasModified got called QCOMPARE(m_uiInterface->dest().toLocalFile(), destFile); checkTestDirectory(srcSubDir()); QVERIFY(!QFile::exists(destSubDir())); } void FileUndoManagerTest::testPasteClipboardUndo() { const QList urls(sourceList()); QMimeData *mimeData = new QMimeData(); mimeData->setUrls(urls); KIO::setClipboardDataCut(mimeData, true); QClipboard *clipboard = QApplication::clipboard(); clipboard->setMimeData(mimeData); // Paste the contents of the clipboard and check its status QUrl destDirUrl = QUrl::fromLocalFile(destDir()); KIO::Job *job = KIO::paste(mimeData, destDirUrl); QVERIFY(job); QVERIFY(job->exec()); // Check if the clipboard was updated after paste operation QList urls2; Q_FOREACH (const QUrl &url, urls) { QUrl dUrl = destDirUrl.adjusted(QUrl::StripTrailingSlash); dUrl.setPath(dUrl.path() + '/' + url.fileName()); urls2 << dUrl; } QList clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData()); QCOMPARE(clipboardUrls, urls2); // Check if the clipboard was updated after undo operation doUndo(); clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData()); QCOMPARE(clipboardUrls, urls); } +void FileUndoManagerTest::testBatchRename() +{ + auto createUrl = [](const QString &path) -> QUrl { + return QUrl::fromLocalFile(homeTmpDir() + path); + }; + + QList srcList; + srcList << createUrl("textfile.txt") << createUrl("mediafile.mkv") << createUrl("sourcefile.cpp"); + + createTestFile(srcList.at(0).path(), "foo"); + createTestFile(srcList.at(1).path(), "foo"); + createTestFile(srcList.at(2).path(), "foo"); + + KIO::Job *job = KIO::batchRename(srcList, QLatin1String("newfile###"), 1, QLatin1Char('#')); + job->setUiDelegate(nullptr); + FileUndoManager::self()->recordJob(FileUndoManager::BatchRename, srcList, QUrl(), job); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + + QVERIFY(QFile::exists(createUrl("newfile001.txt").path())); + QVERIFY(QFile::exists(createUrl("newfile002.mkv").path())); + QVERIFY(QFile::exists(createUrl("newfile003.cpp").path())); + QVERIFY(!QFile::exists(srcList.at(0).path())); + QVERIFY(!QFile::exists(srcList.at(1).path())); + QVERIFY(!QFile::exists(srcList.at(2).path())); + + doUndo(); + + QVERIFY(!QFile::exists(createUrl("newfile###.txt").path())); + QVERIFY(!QFile::exists(createUrl("newfile###.mkv").path())); + QVERIFY(!QFile::exists(createUrl("newfile###.cpp").path())); + QVERIFY(QFile::exists(srcList.at(0).path())); + QVERIFY(QFile::exists(srcList.at(1).path())); + QVERIFY(QFile::exists(srcList.at(2).path())); +} + // TODO: add test (and fix bug) for DND of remote urls / "Link here" (creates .desktop files) // Undo (doesn't do anything) // TODO: add test for interrupting a moving operation and then using Undo - bug:91579 diff --git a/autotests/fileundomanagertest.h b/autotests/fileundomanagertest.h index cd2394b6..6fdb227f 100644 --- a/autotests/fileundomanagertest.h +++ b/autotests/fileundomanagertest.h @@ -1,60 +1,61 @@ /* This file is part of KDE Copyright (c) 2006, 2008 David Faure 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 FILEUNDOMANAGERTEST_H #define FILEUNDOMANAGERTEST_H #include #include class TestUiInterface; class FileUndoManagerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testCopyFiles(); void testMoveFiles(); void testCopyDirectory(); void testMoveDirectory(); void testRenameFile(); void testRenameDir(); void testTrashFiles(); void testRestoreTrashedFiles(); void testModifyFileBeforeUndo(); // #20532 void testCreateSymlink(); void testCreateDir(); void testMkpath(); void testPasteClipboardUndo(); // #318757 + void testBatchRename(); // TODO find tests that would lead to kio job errors // TODO test renaming during a CopyJob. // Doesn't seem possible though, requires user interaction... // TODO: add test for undoing after a partial move (http://bugs.kde.org/show_bug.cgi?id=91579) // Difficult too. private: void doUndo(); TestUiInterface *m_uiInterface; }; #endif diff --git a/src/widgets/fileundomanager.cpp b/src/widgets/fileundomanager.cpp index fe836522..6091df83 100644 --- a/src/widgets/fileundomanager.cpp +++ b/src/widgets/fileundomanager.cpp @@ -1,752 +1,769 @@ /* This file is part of the KDE project Copyright (C) 2000 Simon Hausmann Copyright (C) 2006, 2008 David Faure 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. */ #include "fileundomanager.h" #include "fileundomanager_p.h" #include "clipboardupdater_p.h" #include "fileundomanager_adaptor.h" #include #include #include #include #include +#include #include #include #include #include #include #include #include #include using namespace KIO; #if 0 static const char *undoStateToString(UndoState state) { static const char *const s_undoStateToString[] = { "MAKINGDIRS", "MOVINGFILES", "STATINGFILE", "REMOVINGDIRS", "REMOVINGLINKS" }; return s_undoStateToString[state]; } #endif static QDataStream &operator<<(QDataStream &stream, const KIO::BasicOperation &op) { stream << op.m_valid << (qint8)op.m_type << op.m_renamed << op.m_src << op.m_dst << op.m_target << qint64(op.m_mtime.toMSecsSinceEpoch() / 1000); return stream; } static QDataStream &operator>>(QDataStream &stream, BasicOperation &op) { qint8 type; qint64 mtime; stream >> op.m_valid >> type >> op.m_renamed >> op.m_src >> op.m_dst >> op.m_target >> mtime; op.m_type = static_cast(type); op.m_mtime = QDateTime::fromMSecsSinceEpoch(1000 * mtime); return stream; } static QDataStream &operator<<(QDataStream &stream, const UndoCommand &cmd) { stream << cmd.m_valid << (qint8)cmd.m_type << cmd.m_opStack << cmd.m_src << cmd.m_dst; return stream; } static QDataStream &operator>>(QDataStream &stream, UndoCommand &cmd) { qint8 type; stream >> cmd.m_valid >> type >> cmd.m_opStack >> cmd.m_src >> cmd.m_dst; cmd.m_type = static_cast(type); return stream; } /** * checklist: * copy dir -> overwrite -> works * move dir -> overwrite -> works * copy dir -> rename -> works * move dir -> rename -> works * * copy dir -> works * move dir -> works * * copy files -> works * move files -> works (TODO: optimize (change FileCopyJob to use the renamed arg for copyingDone) * * copy files -> overwrite -> works (sorry for your overwritten file...) * move files -> overwrite -> works (sorry for your overwritten file...) * * copy files -> rename -> works * move files -> rename -> works * * -> see also fileundomanagertest, which tests some of the above (but not renaming). * */ class KIO::UndoJob : public KIO::Job { Q_OBJECT public: UndoJob(bool showProgressInfo) : KIO::Job() { if (showProgressInfo) { KIO::getJobTracker()->registerJob(this); } } virtual ~UndoJob() {} virtual void kill(bool) { FileUndoManager::self()->d->stopUndo(true); KIO::Job::doKill(); } void emitCreatingDir(const QUrl &dir) { emit description(this, i18n("Creating directory"), qMakePair(i18n("Directory"), dir.toDisplayString())); } void emitMoving(const QUrl &src, const QUrl &dest) { emit description(this, i18n("Moving"), qMakePair(i18nc("The source of a file operation", "Source"), src.toDisplayString()), qMakePair(i18nc("The destination of a file operation", "Destination"), dest.toDisplayString())); } void emitDeleting(const QUrl &url) { emit description(this, i18n("Deleting"), qMakePair(i18n("File"), url.toDisplayString())); } void emitResult() { KIO::Job::emitResult(); } }; CommandRecorder::CommandRecorder(FileUndoManager::CommandType op, const QList &src, const QUrl &dst, KIO::Job *job) : QObject(job) { m_cmd.m_type = op; m_cmd.m_valid = true; m_cmd.m_serialNumber = FileUndoManager::self()->newCommandSerialNumber(); m_cmd.m_src = src; m_cmd.m_dst = dst; connect(job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*))); - if (qobject_cast(job)) { connect(job, SIGNAL(copyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool)), this, SLOT(slotCopyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool))); connect(job, SIGNAL(copyingLinkDone(KIO::Job*,QUrl,QString,QUrl)), this, SLOT(slotCopyingLinkDone(KIO::Job*,QUrl,QString,QUrl))); } else if (KIO::MkpathJob *mkpathJob = qobject_cast(job)) { connect(mkpathJob, &KIO::MkpathJob::directoryCreated, this, &CommandRecorder::slotDirectoryCreated); + } else if (KIO::BatchRenameJob *batchRenameJob = qobject_cast(job)) { + connect(batchRenameJob, &KIO::BatchRenameJob::fileRenamed, + this, &CommandRecorder::slotBatchRenamingDone); } } CommandRecorder::~CommandRecorder() { } void CommandRecorder::slotResult(KJob *job) { if (job->error()) { return; } FileUndoManager::self()->d->addCommand(m_cmd); } void CommandRecorder::slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &mtime, bool directory, bool renamed) { BasicOperation op; op.m_valid = true; op.m_type = directory ? BasicOperation::Directory : BasicOperation::File; op.m_renamed = renamed; op.m_src = from; op.m_dst = to; op.m_mtime = mtime; m_cmd.m_opStack.prepend(op); } // TODO merge the signals? void CommandRecorder::slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to) { BasicOperation op; op.m_valid = true; op.m_type = BasicOperation::Link; op.m_renamed = false; op.m_src = from; op.m_target = target; op.m_dst = to; op.m_mtime = QDateTime(); m_cmd.m_opStack.prepend(op); } void CommandRecorder::slotDirectoryCreated(const QUrl &dir) { BasicOperation op; op.m_valid = true; op.m_type = BasicOperation::Directory; op.m_renamed = false; op.m_src = QUrl(); op.m_dst = dir; op.m_mtime = QDateTime(); m_cmd.m_opStack.prepend(op); } +void CommandRecorder::slotBatchRenamingDone(const QUrl &from, const QUrl &to) +{ + BasicOperation op; + op.m_valid = true; + op.m_type = BasicOperation::Directory; + op.m_renamed = true; + op.m_src = from; + op.m_dst = to; + op.m_mtime = QDateTime(); + m_cmd.m_opStack.prepend(op); +} + //// class KIO::FileUndoManagerSingleton { public: FileUndoManager self; }; Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager) FileUndoManager *FileUndoManager::self() { return &globalFileUndoManager()->self; } // m_nextCommandIndex is initialized to a high number so that konqueror can // assign low numbers to closed items loaded "on-demand" from a config file // in KonqClosedWindowsManager::readConfig and thus maintaining the real // order of the undo items. FileUndoManagerPrivate::FileUndoManagerPrivate(FileUndoManager *qq) : m_uiInterface(new FileUndoManager::UiInterface()), m_undoJob(nullptr), m_nextCommandIndex(1000), q(qq) { (void) new KIOFileUndoManagerAdaptor(this); const QString dbusPath = QStringLiteral("/FileUndoManager"); const QString dbusInterface = QStringLiteral("org.kde.kio.FileUndoManager"); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject(dbusPath, this); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("lock"), this, SLOT(slotLock())); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("pop"), this, SLOT(slotPop())); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("push"), this, SLOT(slotPush(QByteArray))); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("unlock"), this, SLOT(slotUnlock())); } FileUndoManager::FileUndoManager() { d = new FileUndoManagerPrivate(this); d->m_lock = false; d->m_currentJob = nullptr; } FileUndoManager::~FileUndoManager() { delete d; } void FileUndoManager::recordJob(CommandType op, const QList &src, const QUrl &dst, KIO::Job *job) { // This records what the job does and calls addCommand when done (void) new CommandRecorder(op, src, dst, job); emit jobRecordingStarted(op); } void FileUndoManager::recordCopyJob(KIO::CopyJob *copyJob) { CommandType commandType; switch (copyJob->operationMode()) { case CopyJob::Copy: commandType = Copy; break; case CopyJob::Move: commandType = Move; break; case CopyJob::Link: default: // prevent "wrong" compiler warning because of possibly uninitialized variable commandType = Link; break; } recordJob(commandType, copyJob->srcUrls(), copyJob->destUrl(), copyJob); } void FileUndoManagerPrivate::addCommand(const UndoCommand &cmd) { pushCommand(cmd); emit q->jobRecordingFinished(cmd.m_type); } bool FileUndoManager::undoAvailable() const { return (d->m_commands.count() > 0) && !d->m_lock; } QString FileUndoManager::undoText() const { if (d->m_commands.isEmpty()) { return i18n("Und&o"); } FileUndoManager::CommandType t = d->m_commands.last().m_type; switch (t) { case FileUndoManager::Copy: return i18n("Und&o: Copy"); case FileUndoManager::Link: return i18n("Und&o: Link"); case FileUndoManager::Move: return i18n("Und&o: Move"); case FileUndoManager::Rename: return i18n("Und&o: Rename"); case FileUndoManager::Trash: return i18n("Und&o: Trash"); case FileUndoManager::Mkdir: return i18n("Und&o: Create Folder"); case FileUndoManager::Mkpath: return i18n("Und&o: Create Folder(s)"); case FileUndoManager::Put: return i18n("Und&o: Create File"); + case FileUndoManager::BatchRename: + return i18n("Und&o: Batch Rename"); } /* NOTREACHED */ return QString(); } quint64 FileUndoManager::newCommandSerialNumber() { return ++(d->m_nextCommandIndex); } quint64 FileUndoManager::currentCommandSerialNumber() const { if (!d->m_commands.isEmpty()) { const UndoCommand &cmd = d->m_commands.last(); assert(cmd.m_valid); return cmd.m_serialNumber; } else { return 0; } } void FileUndoManager::undo() { // Make a copy of the command to undo before slotPop() pops it. UndoCommand cmd = d->m_commands.last(); assert(cmd.m_valid); d->m_current = cmd; const CommandType commandType = cmd.m_type; BasicOperation::Stack &opStack = d->m_current.m_opStack; // Note that opStack is empty for simple operations like Mkdir. // Let's first ask for confirmation if we need to delete any file (#99898) QList itemsToDelete; BasicOperation::Stack::Iterator it = opStack.begin(); for (; it != opStack.end(); ++it) { BasicOperation::Type type = (*it).m_type; if (type == BasicOperation::File && commandType == FileUndoManager::Copy) { itemsToDelete.append((*it).m_dst); } else if (commandType == FileUndoManager::Mkpath) { itemsToDelete.append((*it).m_dst); } } if (commandType == FileUndoManager::Mkdir || commandType == FileUndoManager::Put) { itemsToDelete.append(d->m_current.m_dst); } if (!itemsToDelete.isEmpty()) { if (!d->m_uiInterface->confirmDeletion(itemsToDelete)) { return; } } d->slotPop(); d->slotLock(); d->m_dirCleanupStack.clear(); d->m_dirStack.clear(); d->m_dirsToUpdate.clear(); d->m_undoState = MOVINGFILES; // Let's have a look at the basic operations we need to undo. // While we're at it, collect all links that should be deleted. it = opStack.begin(); while (it != opStack.end()) { // don't cache end() here, erase modifies it bool removeBasicOperation = false; BasicOperation::Type type = (*it).m_type; if (type == BasicOperation::Directory && !(*it).m_renamed) { // If any directory has to be created/deleted, we'll start with that d->m_undoState = MAKINGDIRS; // Collect all the dirs that have to be created in case of a move undo. if (d->m_current.isMoveCommand()) { d->m_dirStack.push((*it).m_src); } // Collect all dirs that have to be deleted // from the destination in both cases (copy and move). d->m_dirCleanupStack.prepend((*it).m_dst); removeBasicOperation = true; } else if (type == BasicOperation::Link) { d->m_fileCleanupStack.prepend((*it).m_dst); removeBasicOperation = !d->m_current.isMoveCommand(); } if (removeBasicOperation) { it = opStack.erase(it); } else { ++it; } } if (commandType == FileUndoManager::Put) { d->m_fileCleanupStack.append(d->m_current.m_dst); } //qDebug() << "starting with" << undoStateToString(d->m_undoState); d->m_undoJob = new UndoJob(d->m_uiInterface->showProgressInfo()); QMetaObject::invokeMethod(d, "undoStep", Qt::QueuedConnection); } void FileUndoManagerPrivate::stopUndo(bool step) { m_current.m_opStack.clear(); m_dirCleanupStack.clear(); m_fileCleanupStack.clear(); m_undoState = REMOVINGDIRS; m_undoJob = nullptr; if (m_currentJob) { m_currentJob->kill(); } m_currentJob = nullptr; if (step) { undoStep(); } } void FileUndoManagerPrivate::slotResult(KJob *job) { m_currentJob = nullptr; if (job->error()) { m_uiInterface->jobError(static_cast(job)); delete m_undoJob; stopUndo(false); } else if (m_undoState == STATINGFILE) { BasicOperation op = m_current.m_opStack.last(); //qDebug() << "stat result for " << op.m_dst; KIO::StatJob *statJob = static_cast(job); const QDateTime mtime = QDateTime::fromMSecsSinceEpoch(1000 * statJob->statResult().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1)); if (mtime != op.m_mtime) { //qDebug() << op.m_dst << " was modified after being copied!"; QDateTime srcTime = op.m_mtime.toLocalTime(); QDateTime destTime = mtime.toLocalTime(); if (!m_uiInterface->copiedFileWasModified(op.m_src, op.m_dst, srcTime, destTime)) { stopUndo(false); } } } undoStep(); } void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url) { if (!m_dirsToUpdate.contains(url)) { m_dirsToUpdate.prepend(url); } } void FileUndoManagerPrivate::undoStep() { m_currentJob = nullptr; if (m_undoState == MAKINGDIRS) { stepMakingDirectories(); } if (m_undoState == MOVINGFILES || m_undoState == STATINGFILE) { stepMovingFiles(); } if (m_undoState == REMOVINGLINKS) { stepRemovingLinks(); } if (m_undoState == REMOVINGDIRS) { stepRemovingDirectories(); } if (m_currentJob) { if (m_uiInterface) { KJobWidgets::setWindow(m_currentJob, m_uiInterface->parentWidget()); } QObject::connect(m_currentJob, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*))); } } void FileUndoManagerPrivate::stepMakingDirectories() { if (!m_dirStack.isEmpty()) { QUrl dir = m_dirStack.pop(); //qDebug() << "creatingDir" << dir; m_currentJob = KIO::mkdir(dir); m_undoJob->emitCreatingDir(dir); } else { m_undoState = MOVINGFILES; } } // Misnamed method: It moves files back, but it also // renames directories back, recreates symlinks, // deletes copied files, and restores trashed files. void FileUndoManagerPrivate::stepMovingFiles() { if (!m_current.m_opStack.isEmpty()) { BasicOperation op = m_current.m_opStack.last(); BasicOperation::Type type = op.m_type; assert(op.m_valid); if (type == BasicOperation::Directory) { if (op.m_renamed) { //qDebug() << "rename" << op.m_dst << op.m_src; m_currentJob = KIO::rename(op.m_dst, op.m_src, KIO::HideProgressInfo); m_undoJob->emitMoving(op.m_dst, op.m_src); } else { assert(0); // this should not happen! } } else if (type == BasicOperation::Link) { //qDebug() << "symlink" << op.m_target << op.m_src; m_currentJob = KIO::symlink(op.m_target, op.m_src, KIO::Overwrite | KIO::HideProgressInfo); } else if (m_current.m_type == FileUndoManager::Copy) { if (m_undoState == MOVINGFILES) { // dest not stat'ed yet // Before we delete op.m_dst, let's check if it was modified (#20532) //qDebug() << "stat" << op.m_dst; m_currentJob = KIO::stat(op.m_dst, KIO::HideProgressInfo); m_undoState = STATINGFILE; // temporarily return; // no pop() yet, we'll finish the work in slotResult } else { // dest was stat'ed, and the deletion was approved in slotResult m_currentJob = KIO::file_delete(op.m_dst, KIO::HideProgressInfo); m_undoJob->emitDeleting(op.m_dst); m_undoState = MOVINGFILES; } } else if (m_current.isMoveCommand() || m_current.m_type == FileUndoManager::Trash) { //qDebug() << "file_move" << op.m_dst << op.m_src; m_currentJob = KIO::file_move(op.m_dst, op.m_src, -1, KIO::Overwrite | KIO::HideProgressInfo); m_currentJob->uiDelegateExtension()->createClipboardUpdater(m_currentJob, JobUiDelegateExtension::UpdateContent); m_undoJob->emitMoving(op.m_dst, op.m_src); } m_current.m_opStack.removeLast(); // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification // So we need to do it ourselves (but schedule it to the end of the undo, to compress them) QUrl url = op.m_dst.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); addDirToUpdate(url); url = op.m_src.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); addDirToUpdate(url); } else { m_undoState = REMOVINGLINKS; } } void FileUndoManagerPrivate::stepRemovingLinks() { //qDebug() << "REMOVINGLINKS"; if (!m_fileCleanupStack.isEmpty()) { QUrl file = m_fileCleanupStack.pop(); //qDebug() << "file_delete" << file; m_currentJob = KIO::file_delete(file, KIO::HideProgressInfo); m_undoJob->emitDeleting(file); QUrl url = file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); addDirToUpdate(url); } else { m_undoState = REMOVINGDIRS; if (m_dirCleanupStack.isEmpty() && m_current.m_type == FileUndoManager::Mkdir) { m_dirCleanupStack << m_current.m_dst; } } } void FileUndoManagerPrivate::stepRemovingDirectories() { if (!m_dirCleanupStack.isEmpty()) { QUrl dir = m_dirCleanupStack.pop(); //qDebug() << "rmdir" << dir; m_currentJob = KIO::rmdir(dir); m_undoJob->emitDeleting(dir); addDirToUpdate(dir); } else { m_current.m_valid = false; m_currentJob = nullptr; if (m_undoJob) { //qDebug() << "deleting undojob"; m_undoJob->emitResult(); m_undoJob = nullptr; } QList::ConstIterator it = m_dirsToUpdate.constBegin(); for (; it != m_dirsToUpdate.constEnd(); ++it) { //qDebug() << "Notifying FilesAdded for " << *it; org::kde::KDirNotify::emitFilesAdded((*it)); } emit q->undoJobFinished(); slotUnlock(); } } // const ref doesn't work due to QDataStream void FileUndoManagerPrivate::slotPush(QByteArray data) { QDataStream strm(&data, QIODevice::ReadOnly); UndoCommand cmd; strm >> cmd; pushCommand(cmd); } void FileUndoManagerPrivate::pushCommand(const UndoCommand &cmd) { m_commands.append(cmd); emit q->undoAvailable(true); emit q->undoTextChanged(q->undoText()); } void FileUndoManagerPrivate::slotPop() { m_commands.removeLast(); emit q->undoAvailable(q->undoAvailable()); emit q->undoTextChanged(q->undoText()); } void FileUndoManagerPrivate::slotLock() { // assert(!m_lock); m_lock = true; emit q->undoAvailable(q->undoAvailable()); } void FileUndoManagerPrivate::slotUnlock() { // assert(m_lock); m_lock = false; emit q->undoAvailable(q->undoAvailable()); } QByteArray FileUndoManagerPrivate::get() const { QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << m_commands; return data; } void FileUndoManager::setUiInterface(UiInterface *ui) { delete d->m_uiInterface; d->m_uiInterface = ui; } FileUndoManager::UiInterface *FileUndoManager::uiInterface() const { return d->m_uiInterface; } //// class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate { public: UiInterfacePrivate() : m_parentWidget(nullptr), m_showProgressInfo(true) {} QWidget *m_parentWidget; bool m_showProgressInfo; }; FileUndoManager::UiInterface::UiInterface() : d(new UiInterfacePrivate) { } FileUndoManager::UiInterface::~UiInterface() { delete d; } void FileUndoManager::UiInterface::jobError(KIO::Job *job) { job->uiDelegate()->showErrorMessage(); } bool FileUndoManager::UiInterface::copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) { Q_UNUSED(srcTime); // not sure it should appear in the msgbox // Possible improvement: only show the time if date is today const QString timeStr = destTime.toString(Qt::DefaultLocaleShortDate); return KMessageBox::warningContinueCancel( d->m_parentWidget, i18n("The file %1 was copied from %2, but since then it has apparently been modified at %3.\n" "Undoing the copy will delete the file, and all modifications will be lost.\n" "Are you sure you want to delete %4?", dest.toDisplayString(QUrl::PreferLocalFile), src.toDisplayString(QUrl::PreferLocalFile), timeStr, dest.toDisplayString(QUrl::PreferLocalFile)), i18n("Undo File Copy Confirmation"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Options(KMessageBox::Notify) | KMessageBox::Dangerous) == KMessageBox::Continue; } bool FileUndoManager::UiInterface::confirmDeletion(const QList &files) { KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(d->m_parentWidget); // Because undo can happen with an accidental Ctrl-Z, we want to always confirm. return uiDelegate.askDeleteConfirmation(files, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::ForceConfirmation); } QWidget *FileUndoManager::UiInterface::parentWidget() const { return d->m_parentWidget; } void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget) { d->m_parentWidget = parentWidget; } void FileUndoManager::UiInterface::setShowProgressInfo(bool b) { d->m_showProgressInfo = b; } bool FileUndoManager::UiInterface::showProgressInfo() const { return d->m_showProgressInfo; } void FileUndoManager::UiInterface::virtual_hook(int, void *) { } #include "moc_fileundomanager_p.cpp" #include "moc_fileundomanager.cpp" #include "fileundomanager.moc" diff --git a/src/widgets/fileundomanager.h b/src/widgets/fileundomanager.h index f43630ae..6cd57b8b 100644 --- a/src/widgets/fileundomanager.h +++ b/src/widgets/fileundomanager.h @@ -1,225 +1,227 @@ /* This file is part of the KDE project Copyright (C) 2000 Simon Hausmann Copyright (C) 2006, 2008 David Faure 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 KIO_FILEUNDOMANAGER_H #define KIO_FILEUNDOMANAGER_H #include #include #include "kiowidgets_export.h" class QDateTime; namespace KIO { class Job; class CopyJob; class FileUndoManagerPrivate; class FileUndoManagerSingleton; class CommandRecorder; class UndoCommand; class UndoJob; /** * @class KIO::FileUndoManager fileundomanager.h * * FileUndoManager: makes it possible to undo kio jobs. * This class is a singleton, use self() to access its only instance. */ class KIOWIDGETS_EXPORT FileUndoManager : public QObject { Q_OBJECT public: /** * @return the FileUndoManager instance */ static FileUndoManager *self(); /** * Interface for the gui handling of FileUndoManager. * This includes three events currently: * - error when undoing a job * - confirm deletion before undoing a copy job * - confirm deletion when the copied file has been modified afterwards * * By default UiInterface shows message boxes in all three cases; * applications can reimplement this interface to provide different user interfaces. */ class KIOWIDGETS_EXPORT UiInterface { public: UiInterface(); virtual ~UiInterface(); /** * Sets whether to show progress info when running the KIO jobs for undoing. */ void setShowProgressInfo(bool b); /** * @returns whether progress info dialogs are shown while undoing. */ bool showProgressInfo() const; /** * Sets the parent widget to use for message boxes. */ void setParentWidget(QWidget *parentWidget); /** * @return the parent widget passed to the last call to undo(parentWidget), or 0. */ QWidget *parentWidget() const; /** * Called when an undo job errors; default implementation displays a message box. */ virtual void jobError(KIO::Job *job); /** * Called when we are about to remove those files. * Return true if we should proceed with deleting them. */ virtual bool confirmDeletion(const QList &files); /** * Called when dest was modified since it was copied from src. * Note that this is called after confirmDeletion. * Return true if we should proceed with deleting dest. */ virtual bool copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime); /** * \internal, for future extensions */ virtual void virtual_hook(int id, void *data); private: class UiInterfacePrivate; UiInterfacePrivate *d; }; /** * Set a new UiInterface implementation. * This deletes the previous one. * @param ui the UiInterface instance, which becomes owned by the undo manager. */ void setUiInterface(UiInterface *ui); /** * @return the UiInterface instance passed to setUiInterface. * This is useful for calling setParentWidget on it. Never delete it! */ UiInterface *uiInterface() const; /** * The type of job. * * Put: @since 4.7, represents the creation of a file from data in memory. * Used when pasting data from clipboard or drag-n-drop. * Mkpath: @since 5.4, represents a KIO::mkpath() job. + * BatchRename: @since 5.42, represents a KIO::batchRename() job. Used when + * renaming multiple files. */ - enum CommandType { Copy, Move, Rename, Link, Mkdir, Trash, Put, Mkpath }; + enum CommandType { Copy, Move, Rename, Link, Mkdir, Trash, Put, Mkpath, BatchRename }; /** * Record this job while it's happening and add a command for it so that the user can undo it. * The signal jobRecordingStarted() is emitted. * @param op the type of job - which is also the type of command that will be created for it * @param src list of source urls. This is empty for Mkdir, Mkpath, Put operations. * @param dst destination url * @param job the job to record */ void recordJob(CommandType op, const QList &src, const QUrl &dst, KIO::Job *job); /** * Record this CopyJob while it's happening and add a command for it so that the user can undo it. * The signal jobRecordingStarted() is emitted. */ void recordCopyJob(KIO::CopyJob *copyJob); /** * @return true if undo is possible. Usually used for enabling/disabling the undo action. */ bool undoAvailable() const; /** * @return the current text for the undo action. */ QString undoText() const; /** * These two functions are useful when wrapping FileUndoManager and adding custom commands. * Each command has a unique ID. You can get a new serial number for a custom command * with newCommandSerialNumber(), and then when you want to undo, check if the command * FileUndoManager would undo is newer or older than your custom command. */ quint64 newCommandSerialNumber(); quint64 currentCommandSerialNumber() const; public Q_SLOTS: /** * Undoes the last command * Remember to call uiInterface()->setParentWidget(parentWidget) first, * if you have multiple mainwindows. * * This operation is asynchronous. * undoJobFinished will be emitted once the undo is complete. */ void undo(); Q_SIGNALS: /// Emitted when the value of undoAvailable() changes void undoAvailable(bool avail); /// Emitted when the value of undoText() changes void undoTextChanged(const QString &text); /// Emitted when an undo job finishes. Used for unit testing. void undoJobFinished(); /** * Emitted when a job recording has been started by FileUndoManager::recordJob() * or FileUndoManager::recordCopyJob(). After the job recording has been finished, * the signal jobRecordingFinished() will be emitted. * @since 4.2 */ void jobRecordingStarted(CommandType op); /** * Emitted when a job that has been recorded by FileUndoManager::recordJob() * or FileUndoManager::recordCopyJob has been finished. The command * is now available for an undo-operation. * @since 4.2 */ void jobRecordingFinished(CommandType op); private: FileUndoManager(); virtual ~FileUndoManager(); friend class FileUndoManagerSingleton; friend class UndoJob; friend class CommandRecorder; friend class FileUndoManagerPrivate; FileUndoManagerPrivate *d; }; } // namespace #endif diff --git a/src/widgets/fileundomanager_p.h b/src/widgets/fileundomanager_p.h index 1832b8aa..91f88172 100644 --- a/src/widgets/fileundomanager_p.h +++ b/src/widgets/fileundomanager_p.h @@ -1,178 +1,179 @@ /* This file is part of the KDE project Copyright (C) 2000 Simon Hausmann Copyright (C) 2006, 2008 David Faure 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 FILEUNDOMANAGER_P_H #define FILEUNDOMANAGER_P_H #include "fileundomanager.h" #include #include #include class KJob; namespace KIO { class FileUndoManagerAdaptor; struct BasicOperation { typedef QList Stack; BasicOperation() { m_valid = false; } bool m_valid; bool m_renamed; enum Type { File, Link, Directory }; Type m_type: 2; QUrl m_src; QUrl m_dst; QString m_target; QDateTime m_mtime; }; class UndoCommand { public: typedef QList Stack; UndoCommand() { m_valid = false; } // TODO: is ::TRASH missing? bool isMoveCommand() const { return m_type == FileUndoManager::Move || m_type == FileUndoManager::Rename; } bool m_valid; FileUndoManager::CommandType m_type; BasicOperation::Stack m_opStack; QList m_src; QUrl m_dst; quint64 m_serialNumber; }; // This class listens to a job, collects info while it's running (for copyjobs) // and when the job terminates, on success, it calls addCommand in the undomanager. class CommandRecorder : public QObject { Q_OBJECT public: CommandRecorder(FileUndoManager::CommandType op, const QList &src, const QUrl &dst, KIO::Job *job); virtual ~CommandRecorder(); private Q_SLOTS: void slotResult(KJob *job); void slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &, bool directory, bool renamed); void slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to); void slotDirectoryCreated(const QUrl &url); + void slotBatchRenamingDone(const QUrl &from, const QUrl &to); private: UndoCommand m_cmd; }; enum UndoState { MAKINGDIRS = 0, MOVINGFILES, STATINGFILE, REMOVINGDIRS, REMOVINGLINKS }; // The private class is, exceptionally, a real QObject // so that it can be the class with the DBUS adaptor forwarding its signals. class FileUndoManagerPrivate : public QObject { Q_OBJECT public: FileUndoManagerPrivate(FileUndoManager *qq); ~FileUndoManagerPrivate() { delete m_uiInterface; } void pushCommand(const UndoCommand &cmd); void addDirToUpdate(const QUrl &url); void stepMakingDirectories(); void stepMovingFiles(); void stepRemovingLinks(); void stepRemovingDirectories(); /// called by FileUndoManagerAdaptor QByteArray get() const; friend class UndoJob; /// called by UndoJob void stopUndo(bool step); friend class UndoCommandRecorder; /// called by UndoCommandRecorder void addCommand(const UndoCommand &cmd); bool m_lock; UndoCommand::Stack m_commands; UndoCommand m_current; KIO::Job *m_currentJob; UndoState m_undoState; QStack m_dirStack; QStack m_dirCleanupStack; QStack m_fileCleanupStack; // files and links QList m_dirsToUpdate; FileUndoManager::UiInterface *m_uiInterface; UndoJob *m_undoJob; quint64 m_nextCommandIndex; FileUndoManager *q; // DBUS interface Q_SIGNALS: /// DBUS signal void push(const QByteArray &command); /// DBUS signal void pop(); /// DBUS signal void lock(); /// DBUS signal void unlock(); public Q_SLOTS: // Those four slots are connected to DBUS signals void slotPush(QByteArray); void slotPop(); void slotLock(); void slotUnlock(); void undoStep(); void slotResult(KJob *); }; } // namespace #endif /* FILEUNDOMANAGER_P_H */