File indexing completed on 2024-04-21 03:55:43

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
0004     SPDX-FileCopyrightText: 2006, 2008 David Faure <faure@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "fileundomanager.h"
0010 #include "askuseractioninterface.h"
0011 #include "clipboardupdater_p.h"
0012 #include "fileundomanager_adaptor.h"
0013 #include "fileundomanager_p.h"
0014 #include "kio_widgets_debug.h"
0015 #include <job_p.h>
0016 #include <kdirnotify.h>
0017 #include <kio/batchrenamejob.h>
0018 #include <kio/copyjob.h>
0019 #include <kio/filecopyjob.h>
0020 #include <kio/jobuidelegate.h>
0021 #include <kio/mkdirjob.h>
0022 #include <kio/mkpathjob.h>
0023 #include <kio/statjob.h>
0024 
0025 #include <KJobTrackerInterface>
0026 #include <KJobWidgets>
0027 #include <KLocalizedString>
0028 #include <KMessageBox>
0029 
0030 #include <QDBusConnection>
0031 #include <QDateTime>
0032 #include <QFileInfo>
0033 #include <QLocale>
0034 
0035 using namespace KIO;
0036 
0037 static const char *undoStateToString(UndoState state)
0038 {
0039     static const char *const s_undoStateToString[] = {"MAKINGDIRS", "MOVINGFILES", "STATINGFILE", "REMOVINGDIRS", "REMOVINGLINKS"};
0040     return s_undoStateToString[state];
0041 }
0042 
0043 static QDataStream &operator<<(QDataStream &stream, const KIO::BasicOperation &op)
0044 {
0045     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);
0046     return stream;
0047 }
0048 static QDataStream &operator>>(QDataStream &stream, BasicOperation &op)
0049 {
0050     qint8 type;
0051     qint64 mtime;
0052     stream >> op.m_valid >> type >> op.m_renamed >> op.m_src >> op.m_dst >> op.m_target >> mtime;
0053     op.m_type = static_cast<BasicOperation::Type>(type);
0054     op.m_mtime = QDateTime::fromSecsSinceEpoch(mtime, QTimeZone::UTC);
0055     return stream;
0056 }
0057 
0058 static QDataStream &operator<<(QDataStream &stream, const UndoCommand &cmd)
0059 {
0060     stream << cmd.m_valid << (qint8)cmd.m_type << cmd.m_opQueue << cmd.m_src << cmd.m_dst;
0061     return stream;
0062 }
0063 
0064 static QDataStream &operator>>(QDataStream &stream, UndoCommand &cmd)
0065 {
0066     qint8 type;
0067     stream >> cmd.m_valid >> type >> cmd.m_opQueue >> cmd.m_src >> cmd.m_dst;
0068     cmd.m_type = static_cast<FileUndoManager::CommandType>(type);
0069     return stream;
0070 }
0071 
0072 QDebug operator<<(QDebug dbg, const BasicOperation &op)
0073 {
0074     if (op.m_valid) {
0075         static const char *s_types[] = {"File", "Link", "Directory"};
0076         dbg << "BasicOperation: type" << s_types[op.m_type] << "src" << op.m_src << "dest" << op.m_dst << "target" << op.m_target << "renamed" << op.m_renamed;
0077     } else {
0078         dbg << "Invalid BasicOperation";
0079     }
0080     return dbg;
0081 }
0082 /**
0083  * checklist:
0084  * copy dir -> overwrite -> works
0085  * move dir -> overwrite -> works
0086  * copy dir -> rename -> works
0087  * move dir -> rename -> works
0088  *
0089  * copy dir -> works
0090  * move dir -> works
0091  *
0092  * copy files -> works
0093  * move files -> works (TODO: optimize (change FileCopyJob to use the renamed arg for copyingDone)
0094  *
0095  * copy files -> overwrite -> works (sorry for your overwritten file...)
0096  * move files -> overwrite -> works (sorry for your overwritten file...)
0097  *
0098  * copy files -> rename -> works
0099  * move files -> rename -> works
0100  *
0101  * -> see also fileundomanagertest, which tests some of the above (but not renaming).
0102  *
0103  */
0104 
0105 class KIO::UndoJob : public KIO::Job
0106 {
0107     Q_OBJECT
0108 public:
0109     UndoJob(bool showProgressInfo)
0110         : KIO::Job()
0111     {
0112         if (showProgressInfo) {
0113             KIO::getJobTracker()->registerJob(this);
0114         }
0115 
0116         d_ptr->m_privilegeExecutionEnabled = true;
0117         d_ptr->m_operationType = d_ptr->Other;
0118         d_ptr->m_title = i18n("Undo Changes");
0119         d_ptr->m_message = i18n("Undoing this operation requires root privileges. Do you want to continue?");
0120     }
0121 
0122     ~UndoJob() override = default;
0123 
0124     virtual void kill(bool) // TODO should be doKill
0125     {
0126         FileUndoManager::self()->d->stopUndo(true);
0127         KIO::Job::doKill();
0128     }
0129 
0130     void emitCreatingDir(const QUrl &dir)
0131     {
0132         Q_EMIT description(this, i18n("Creating directory"), qMakePair(i18n("Directory"), dir.toDisplayString()));
0133     }
0134 
0135     void emitMovingOrRenaming(const QUrl &src, const QUrl &dest, FileUndoManager::CommandType cmdType)
0136     {
0137         static const QString srcMsg(i18nc("The source of a file operation", "Source"));
0138         static const QString destMsg(i18nc("The destination of a file operation", "Destination"));
0139 
0140         Q_EMIT description(this, //
0141                            cmdType == FileUndoManager::Move ? i18n("Moving") : i18n("Renaming"),
0142                            {srcMsg, src.toDisplayString()},
0143                            {destMsg, dest.toDisplayString()});
0144     }
0145 
0146     void emitDeleting(const QUrl &url)
0147     {
0148         Q_EMIT description(this, i18n("Deleting"), qMakePair(i18n("File"), url.toDisplayString()));
0149     }
0150     void emitResult()
0151     {
0152         KIO::Job::emitResult();
0153     }
0154 };
0155 
0156 CommandRecorder::CommandRecorder(FileUndoManager::CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job)
0157     : QObject(job)
0158     , m_cmd(op, src, dst, FileUndoManager::self()->newCommandSerialNumber())
0159 {
0160     connect(job, &KJob::result, this, &CommandRecorder::slotResult);
0161     if (auto *copyJob = qobject_cast<KIO::CopyJob *>(job)) {
0162         connect(copyJob, &KIO::CopyJob::copyingDone, this, &CommandRecorder::slotCopyingDone);
0163         connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, &CommandRecorder::slotCopyingLinkDone);
0164     } else if (auto *mkpathJob = qobject_cast<KIO::MkpathJob *>(job)) {
0165         connect(mkpathJob, &KIO::MkpathJob::directoryCreated, this, &CommandRecorder::slotDirectoryCreated);
0166     } else if (auto *batchRenameJob = qobject_cast<KIO::BatchRenameJob *>(job)) {
0167         connect(batchRenameJob, &KIO::BatchRenameJob::fileRenamed, this, &CommandRecorder::slotBatchRenamingDone);
0168     }
0169 }
0170 
0171 void CommandRecorder::slotResult(KJob *job)
0172 {
0173     const int err = job->error();
0174     if (err) {
0175         if (err != KIO::ERR_USER_CANCELED) {
0176             qCDebug(KIO_WIDGETS) << "CommandRecorder::slotResult:" << job->errorString() << " - no undo command will be added";
0177         }
0178         return;
0179     }
0180 
0181     // For CopyJob, don't add an undo command unless the job actually did something,
0182     // e.g. if user selected to skip all, there is nothing to undo.
0183     // Note: this doesn't apply to other job types, e.g. for Mkdir m_opQueue is
0184     // expected to be empty
0185     if (qobject_cast<KIO::CopyJob *>(job)) {
0186         if (!m_cmd.m_opQueue.isEmpty()) {
0187             FileUndoManager::self()->d->addCommand(m_cmd);
0188         }
0189         return;
0190     }
0191 
0192     FileUndoManager::self()->d->addCommand(m_cmd);
0193 }
0194 
0195 void CommandRecorder::slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &mtime, bool directory, bool renamed)
0196 {
0197     const BasicOperation::Type type = directory ? BasicOperation::Directory : BasicOperation::File;
0198     m_cmd.m_opQueue.enqueue(BasicOperation(type, renamed, from, to, mtime));
0199 }
0200 
0201 void CommandRecorder::slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to)
0202 {
0203     m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Link, false, from, to, {}, target));
0204 }
0205 
0206 void CommandRecorder::slotDirectoryCreated(const QUrl &dir)
0207 {
0208     m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Directory, false, QUrl{}, dir, {}));
0209 }
0210 
0211 void CommandRecorder::slotBatchRenamingDone(const QUrl &from, const QUrl &to)
0212 {
0213     m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Item, true, from, to, {}));
0214 }
0215 
0216 ////
0217 
0218 class KIO::FileUndoManagerSingleton
0219 {
0220 public:
0221     FileUndoManager self;
0222 };
0223 Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager)
0224 
0225 FileUndoManager *FileUndoManager::self()
0226 {
0227     return &globalFileUndoManager()->self;
0228 }
0229 
0230 // m_nextCommandIndex is initialized to a high number so that konqueror can
0231 // assign low numbers to closed items loaded "on-demand" from a config file
0232 // in KonqClosedWindowsManager::readConfig and thus maintaining the real
0233 // order of the undo items.
0234 FileUndoManagerPrivate::FileUndoManagerPrivate(FileUndoManager *qq)
0235     : m_uiInterface(new FileUndoManager::UiInterface())
0236     , m_nextCommandIndex(1000)
0237     , q(qq)
0238 {
0239 #if !defined(Q_OS_WIN) && !defined(Q_OS_MAC)
0240     (void)new KIOFileUndoManagerAdaptor(this);
0241     const QString dbusPath = QStringLiteral("/FileUndoManager");
0242     const QString dbusInterface = QStringLiteral("org.kde.kio.FileUndoManager");
0243 
0244     QDBusConnection dbus = QDBusConnection::sessionBus();
0245     dbus.registerObject(dbusPath, this);
0246     dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("lock"), this, SLOT(slotLock()));
0247     dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("pop"), this, SLOT(slotPop()));
0248     dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("push"), this, SLOT(slotPush(QByteArray)));
0249     dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("unlock"), this, SLOT(slotUnlock()));
0250 #endif
0251 }
0252 
0253 FileUndoManager::FileUndoManager()
0254     : d(new FileUndoManagerPrivate(this))
0255 {
0256 }
0257 
0258 FileUndoManager::~FileUndoManager() = default;
0259 
0260 void FileUndoManager::recordJob(CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job)
0261 {
0262     // This records what the job does and calls addCommand when done
0263     (void)new CommandRecorder(op, src, dst, job);
0264     Q_EMIT jobRecordingStarted(op);
0265 }
0266 
0267 void FileUndoManager::recordCopyJob(KIO::CopyJob *copyJob)
0268 {
0269     CommandType commandType;
0270     switch (copyJob->operationMode()) {
0271     case CopyJob::Copy:
0272         commandType = Copy;
0273         break;
0274     case CopyJob::Move:
0275         commandType = Move;
0276         break;
0277     case CopyJob::Link:
0278         commandType = Link;
0279         break;
0280     default:
0281         Q_UNREACHABLE();
0282     }
0283     recordJob(commandType, copyJob->srcUrls(), copyJob->destUrl(), copyJob);
0284 }
0285 
0286 void FileUndoManagerPrivate::addCommand(const UndoCommand &cmd)
0287 {
0288     pushCommand(cmd);
0289     Q_EMIT q->jobRecordingFinished(cmd.m_type);
0290 }
0291 
0292 bool FileUndoManager::isUndoAvailable() const
0293 {
0294     return !d->m_commands.isEmpty() && !d->m_lock;
0295 }
0296 
0297 QString FileUndoManager::undoText() const
0298 {
0299     if (d->m_commands.isEmpty()) {
0300         return i18n("Und&o");
0301     }
0302 
0303     FileUndoManager::CommandType t = d->m_commands.top().m_type;
0304     switch (t) {
0305     case FileUndoManager::Copy:
0306         return i18n("Und&o: Copy");
0307     case FileUndoManager::Link:
0308         return i18n("Und&o: Link");
0309     case FileUndoManager::Move:
0310         return i18n("Und&o: Move");
0311     case FileUndoManager::Rename:
0312         return i18n("Und&o: Rename");
0313     case FileUndoManager::Trash:
0314         return i18n("Und&o: Trash");
0315     case FileUndoManager::Mkdir:
0316         return i18n("Und&o: Create Folder");
0317     case FileUndoManager::Mkpath:
0318         return i18n("Und&o: Create Folder(s)");
0319     case FileUndoManager::Put:
0320         return i18n("Und&o: Create File");
0321     case FileUndoManager::BatchRename:
0322         return i18n("Und&o: Batch Rename");
0323     }
0324     /* NOTREACHED */
0325     return QString();
0326 }
0327 
0328 quint64 FileUndoManager::newCommandSerialNumber()
0329 {
0330     return ++(d->m_nextCommandIndex);
0331 }
0332 
0333 quint64 FileUndoManager::currentCommandSerialNumber() const
0334 {
0335     if (!d->m_commands.isEmpty()) {
0336         const UndoCommand &cmd = d->m_commands.top();
0337         Q_ASSERT(cmd.m_valid);
0338         return cmd.m_serialNumber;
0339     }
0340 
0341     return 0;
0342 }
0343 
0344 void FileUndoManager::undo()
0345 {
0346     Q_ASSERT(!d->m_commands.isEmpty()); // forgot to record before calling undo?
0347 
0348     // Make a copy of the command to undo before slotPop() pops it.
0349     UndoCommand cmd = d->m_commands.last();
0350     Q_ASSERT(cmd.m_valid);
0351     d->m_currentCmd = cmd;
0352     const CommandType commandType = cmd.m_type;
0353 
0354     // Note that m_opQueue is empty for simple operations like Mkdir.
0355     const auto &opQueue = d->m_currentCmd.m_opQueue;
0356 
0357     // Let's first ask for confirmation if we need to delete any file (#99898)
0358     QList<QUrl> itemsToDelete;
0359     for (auto it = opQueue.crbegin(); it != opQueue.crend(); ++it) {
0360         const BasicOperation &op = *it;
0361         const auto destination = op.m_dst;
0362         if (op.m_type == BasicOperation::File && commandType == FileUndoManager::Copy) {
0363             if (destination.isLocalFile() && !QFileInfo::exists(destination.toLocalFile())) {
0364                 continue;
0365             }
0366             itemsToDelete.append(destination);
0367         } else if (commandType == FileUndoManager::Mkpath) {
0368             itemsToDelete.append(destination);
0369         }
0370     }
0371     if (commandType == FileUndoManager::Mkdir || commandType == FileUndoManager::Put) {
0372         itemsToDelete.append(d->m_currentCmd.m_dst);
0373     }
0374     if (!itemsToDelete.isEmpty()) {
0375         AskUserActionInterface *askUserInterface = nullptr;
0376         d->m_uiInterface->virtual_hook(UiInterface::HookGetAskUserActionInterface, &askUserInterface);
0377         if (askUserInterface) {
0378             if (!d->m_connectedToAskUserInterface) {
0379                 d->m_connectedToAskUserInterface = true;
0380                 QObject::connect(askUserInterface, &KIO::AskUserActionInterface::askUserDeleteResult, this, [=](bool allowDelete) {
0381                     if (allowDelete) {
0382                         d->startUndo();
0383                     }
0384                 });
0385             }
0386 
0387             // Because undo can happen with an accidental Ctrl-Z, we want to always confirm.
0388             askUserInterface->askUserDelete(itemsToDelete,
0389                                             KIO::AskUserActionInterface::Delete,
0390                                             KIO::AskUserActionInterface::ForceConfirmation,
0391                                             d->m_uiInterface->parentWidget());
0392             return;
0393         }
0394     }
0395 
0396     d->startUndo();
0397 }
0398 
0399 void FileUndoManagerPrivate::startUndo()
0400 {
0401     slotPop();
0402     slotLock();
0403 
0404     m_dirCleanupStack.clear();
0405     m_dirStack.clear();
0406     m_dirsToUpdate.clear();
0407 
0408     m_undoState = MOVINGFILES;
0409 
0410     // Let's have a look at the basic operations we need to undo.
0411     auto &opQueue = m_currentCmd.m_opQueue;
0412     for (auto it = opQueue.rbegin(); it != opQueue.rend(); ++it) {
0413         const BasicOperation::Type type = (*it).m_type;
0414         if (type == BasicOperation::Directory && !(*it).m_renamed) {
0415             // If any directory has to be created/deleted, we'll start with that
0416             m_undoState = MAKINGDIRS;
0417             // Collect all the dirs that have to be created in case of a move undo.
0418             if (m_currentCmd.isMoveOrRename()) {
0419                 m_dirStack.push((*it).m_src);
0420             }
0421             // Collect all dirs that have to be deleted
0422             // from the destination in both cases (copy and move).
0423             m_dirCleanupStack.prepend((*it).m_dst);
0424         } else if (type == BasicOperation::Link) {
0425             m_fileCleanupStack.prepend((*it).m_dst);
0426         }
0427     }
0428     auto isBasicOperation = [this](const BasicOperation &op) {
0429         return (op.m_type == BasicOperation::Directory && !op.m_renamed) //
0430             || (op.m_type == BasicOperation::Link && !m_currentCmd.isMoveOrRename());
0431     };
0432     opQueue.erase(std::remove_if(opQueue.begin(), opQueue.end(), isBasicOperation), opQueue.end());
0433 
0434     const FileUndoManager::CommandType commandType = m_currentCmd.m_type;
0435     if (commandType == FileUndoManager::Put) {
0436         m_fileCleanupStack.append(m_currentCmd.m_dst);
0437     }
0438 
0439     qCDebug(KIO_WIDGETS) << "starting with" << undoStateToString(m_undoState);
0440     m_undoJob = new UndoJob(m_uiInterface->showProgressInfo());
0441     auto undoFunc = [this]() {
0442         undoStep();
0443     };
0444     QMetaObject::invokeMethod(this, undoFunc, Qt::QueuedConnection);
0445 }
0446 
0447 void FileUndoManagerPrivate::stopUndo(bool step)
0448 {
0449     m_currentCmd.m_opQueue.clear();
0450     m_dirCleanupStack.clear();
0451     m_fileCleanupStack.clear();
0452     m_undoState = REMOVINGDIRS;
0453     m_undoJob = nullptr;
0454 
0455     if (m_currentJob) {
0456         m_currentJob->kill();
0457     }
0458 
0459     m_currentJob = nullptr;
0460 
0461     if (step) {
0462         undoStep();
0463     }
0464 }
0465 
0466 void FileUndoManagerPrivate::slotResult(KJob *job)
0467 {
0468     m_currentJob = nullptr;
0469     if (job->error()) {
0470         qWarning() << job->errorString();
0471         m_uiInterface->jobError(static_cast<KIO::Job *>(job));
0472         delete m_undoJob;
0473         stopUndo(false);
0474     } else if (m_undoState == STATINGFILE) {
0475         const BasicOperation op = m_currentCmd.m_opQueue.head();
0476         // qDebug() << "stat result for " << op.m_dst;
0477         KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
0478         const QDateTime mtime = QDateTime::fromSecsSinceEpoch(statJob->statResult().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), QTimeZone::UTC);
0479         if (mtime != op.m_mtime) {
0480             qCDebug(KIO_WIDGETS) << op.m_dst << "was modified after being copied. Initial timestamp" << mtime << "now" << op.m_mtime;
0481             QDateTime srcTime = op.m_mtime.toLocalTime();
0482             QDateTime destTime = mtime.toLocalTime();
0483             if (!m_uiInterface->copiedFileWasModified(op.m_src, op.m_dst, srcTime, destTime)) {
0484                 stopUndo(false);
0485             }
0486         }
0487     }
0488 
0489     undoStep();
0490 }
0491 
0492 void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url)
0493 {
0494     if (!m_dirsToUpdate.contains(url)) {
0495         m_dirsToUpdate.prepend(url);
0496     }
0497 }
0498 
0499 void FileUndoManagerPrivate::undoStep()
0500 {
0501     m_currentJob = nullptr;
0502 
0503     if (m_undoState == MAKINGDIRS) {
0504         stepMakingDirectories();
0505     }
0506 
0507     if (m_undoState == MOVINGFILES || m_undoState == STATINGFILE) {
0508         stepMovingFiles();
0509     }
0510 
0511     if (m_undoState == REMOVINGLINKS) {
0512         stepRemovingLinks();
0513     }
0514 
0515     if (m_undoState == REMOVINGDIRS) {
0516         stepRemovingDirectories();
0517     }
0518 
0519     if (m_currentJob) {
0520         if (m_uiInterface) {
0521             KJobWidgets::setWindow(m_currentJob, m_uiInterface->parentWidget());
0522         }
0523         QObject::connect(m_currentJob, &KJob::result, this, &FileUndoManagerPrivate::slotResult);
0524     }
0525 }
0526 
0527 void FileUndoManagerPrivate::stepMakingDirectories()
0528 {
0529     if (!m_dirStack.isEmpty()) {
0530         QUrl dir = m_dirStack.pop();
0531         // qDebug() << "creatingDir" << dir;
0532         m_currentJob = KIO::mkdir(dir);
0533         m_currentJob->setParentJob(m_undoJob);
0534         m_undoJob->emitCreatingDir(dir);
0535     } else {
0536         m_undoState = MOVINGFILES;
0537     }
0538 }
0539 
0540 // Misnamed method: It moves files back, but it also
0541 // renames directories back, recreates symlinks,
0542 // deletes copied files, and restores trashed files.
0543 void FileUndoManagerPrivate::stepMovingFiles()
0544 {
0545     if (m_currentCmd.m_opQueue.isEmpty()) {
0546         m_undoState = REMOVINGLINKS;
0547         return;
0548     }
0549 
0550     const BasicOperation op = m_currentCmd.m_opQueue.head();
0551     Q_ASSERT(op.m_valid);
0552     if (op.m_type == BasicOperation::Directory || op.m_type == BasicOperation::Item) {
0553         Q_ASSERT(op.m_renamed);
0554         // qDebug() << "rename" << op.m_dst << op.m_src;
0555         m_currentJob = KIO::rename(op.m_dst, op.m_src, KIO::HideProgressInfo);
0556         m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type);
0557     } else if (op.m_type == BasicOperation::Link) {
0558         // qDebug() << "symlink" << op.m_target << op.m_src;
0559         m_currentJob = KIO::symlink(op.m_target, op.m_src, KIO::Overwrite | KIO::HideProgressInfo);
0560     } else if (m_currentCmd.m_type == FileUndoManager::Copy) {
0561         if (m_undoState == MOVINGFILES) { // dest not stat'ed yet
0562             // Before we delete op.m_dst, let's check if it was modified (#20532)
0563             // qDebug() << "stat" << op.m_dst;
0564             m_currentJob = KIO::stat(op.m_dst, KIO::HideProgressInfo);
0565             m_undoState = STATINGFILE; // temporarily
0566             return; // no pop() yet, we'll finish the work in slotResult
0567         } else { // dest was stat'ed, and the deletion was approved in slotResult
0568             m_currentJob = KIO::file_delete(op.m_dst, KIO::HideProgressInfo);
0569             m_undoJob->emitDeleting(op.m_dst);
0570             m_undoState = MOVINGFILES;
0571         }
0572     } else if (m_currentCmd.isMoveOrRename() || m_currentCmd.m_type == FileUndoManager::Trash) {
0573         m_currentJob = KIO::file_move(op.m_dst, op.m_src, -1, KIO::HideProgressInfo);
0574         m_currentJob->uiDelegateExtension()->createClipboardUpdater(m_currentJob, JobUiDelegateExtension::UpdateContent);
0575         m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type);
0576     }
0577 
0578     if (m_currentJob) {
0579         m_currentJob->setParentJob(m_undoJob);
0580     }
0581 
0582     m_currentCmd.m_opQueue.dequeue();
0583     // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification
0584     // So we need to do it ourselves (but schedule it to the end of the undo, to compress them)
0585     QUrl url = op.m_dst.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0586     addDirToUpdate(url);
0587 
0588     url = op.m_src.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0589     addDirToUpdate(url);
0590 }
0591 
0592 void FileUndoManagerPrivate::stepRemovingLinks()
0593 {
0594     // qDebug() << "REMOVINGLINKS";
0595     if (!m_fileCleanupStack.isEmpty()) {
0596         const QUrl file = m_fileCleanupStack.pop();
0597         // qDebug() << "file_delete" << file;
0598         m_currentJob = KIO::file_delete(file, KIO::HideProgressInfo);
0599         m_currentJob->setParentJob(m_undoJob);
0600         m_undoJob->emitDeleting(file);
0601 
0602         const QUrl url = file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0603         addDirToUpdate(url);
0604     } else {
0605         m_undoState = REMOVINGDIRS;
0606 
0607         if (m_dirCleanupStack.isEmpty() && m_currentCmd.m_type == FileUndoManager::Mkdir) {
0608             m_dirCleanupStack << m_currentCmd.m_dst;
0609         }
0610     }
0611 }
0612 
0613 void FileUndoManagerPrivate::stepRemovingDirectories()
0614 {
0615     if (!m_dirCleanupStack.isEmpty()) {
0616         QUrl dir = m_dirCleanupStack.pop();
0617         // qDebug() << "rmdir" << dir;
0618         m_currentJob = KIO::rmdir(dir);
0619         m_currentJob->setParentJob(m_undoJob);
0620         m_undoJob->emitDeleting(dir);
0621         addDirToUpdate(dir);
0622     } else {
0623         m_currentCmd.m_valid = false;
0624         m_currentJob = nullptr;
0625         if (m_undoJob) {
0626             // qDebug() << "deleting undojob";
0627             m_undoJob->emitResult();
0628             m_undoJob = nullptr;
0629         }
0630         for (const QUrl &url : std::as_const(m_dirsToUpdate)) {
0631             // qDebug() << "Notifying FilesAdded for " << url;
0632             org::kde::KDirNotify::emitFilesAdded(url);
0633         }
0634         Q_EMIT q->undoJobFinished();
0635         slotUnlock();
0636     }
0637 }
0638 
0639 // const ref doesn't work due to QDataStream
0640 void FileUndoManagerPrivate::slotPush(QByteArray data)
0641 {
0642     QDataStream strm(&data, QIODevice::ReadOnly);
0643     UndoCommand cmd;
0644     strm >> cmd;
0645     pushCommand(cmd);
0646 }
0647 
0648 void FileUndoManagerPrivate::pushCommand(const UndoCommand &cmd)
0649 {
0650     m_commands.push(cmd);
0651     Q_EMIT q->undoAvailable(true);
0652     Q_EMIT q->undoTextChanged(q->undoText());
0653 }
0654 
0655 void FileUndoManagerPrivate::slotPop()
0656 {
0657     m_commands.pop();
0658     Q_EMIT q->undoAvailable(q->isUndoAvailable());
0659     Q_EMIT q->undoTextChanged(q->undoText());
0660 }
0661 
0662 void FileUndoManagerPrivate::slotLock()
0663 {
0664     //  Q_ASSERT(!m_lock);
0665     m_lock = true;
0666     Q_EMIT q->undoAvailable(q->isUndoAvailable());
0667 }
0668 
0669 void FileUndoManagerPrivate::slotUnlock()
0670 {
0671     //  Q_ASSERT(m_lock);
0672     m_lock = false;
0673     Q_EMIT q->undoAvailable(q->isUndoAvailable());
0674 }
0675 
0676 QByteArray FileUndoManagerPrivate::get() const
0677 {
0678     QByteArray data;
0679     QDataStream stream(&data, QIODevice::WriteOnly);
0680     stream << m_commands;
0681     return data;
0682 }
0683 
0684 void FileUndoManager::setUiInterface(UiInterface *ui)
0685 {
0686     d->m_uiInterface.reset(ui);
0687 }
0688 
0689 FileUndoManager::UiInterface *FileUndoManager::uiInterface() const
0690 {
0691     return d->m_uiInterface.get();
0692 }
0693 
0694 ////
0695 
0696 class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate
0697 {
0698 public:
0699     QPointer<QWidget> m_parentWidget;
0700     bool m_showProgressInfo = true;
0701 };
0702 
0703 FileUndoManager::UiInterface::UiInterface()
0704     : d(new UiInterfacePrivate)
0705 {
0706 }
0707 
0708 FileUndoManager::UiInterface::~UiInterface() = default;
0709 
0710 void FileUndoManager::UiInterface::jobError(KIO::Job *job)
0711 {
0712     job->uiDelegate()->showErrorMessage();
0713 }
0714 
0715 bool FileUndoManager::UiInterface::copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime)
0716 {
0717     Q_UNUSED(srcTime); // not sure it should appear in the msgbox
0718     // Possible improvement: only show the time if date is today
0719     const QString timeStr = QLocale().toString(destTime, QLocale::ShortFormat);
0720     const QString msg = i18n(
0721         "The file %1 was copied from %2, but since then it has apparently been modified at %3.\n"
0722         "Undoing the copy will delete the file, and all modifications will be lost.\n"
0723         "Are you sure you want to delete %4?",
0724         dest.toDisplayString(QUrl::PreferLocalFile),
0725         src.toDisplayString(QUrl::PreferLocalFile),
0726         timeStr,
0727         dest.toDisplayString(QUrl::PreferLocalFile));
0728 
0729     const auto result = KMessageBox::warningContinueCancel(d->m_parentWidget,
0730                                                            msg,
0731                                                            i18n("Undo File Copy Confirmation"),
0732                                                            KStandardGuiItem::cont(),
0733                                                            KStandardGuiItem::cancel(),
0734                                                            QString(),
0735                                                            KMessageBox::Options(KMessageBox::Notify) | KMessageBox::Dangerous);
0736     return result == KMessageBox::Continue;
0737 }
0738 
0739 QWidget *FileUndoManager::UiInterface::parentWidget() const
0740 {
0741     return d->m_parentWidget;
0742 }
0743 
0744 void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget)
0745 {
0746     d->m_parentWidget = parentWidget;
0747 }
0748 
0749 void FileUndoManager::UiInterface::setShowProgressInfo(bool b)
0750 {
0751     d->m_showProgressInfo = b;
0752 }
0753 
0754 bool FileUndoManager::UiInterface::showProgressInfo() const
0755 {
0756     return d->m_showProgressInfo;
0757 }
0758 
0759 void FileUndoManager::UiInterface::virtual_hook(int id, void *data)
0760 {
0761     if (id == HookGetAskUserActionInterface) {
0762         auto *p = static_cast<AskUserActionInterface **>(data);
0763         static KJobUiDelegate *delegate = KIO::createDefaultJobUiDelegate();
0764         static auto *askUserInterface = delegate ? delegate->findChild<AskUserActionInterface *>(QString(), Qt::FindDirectChildrenOnly) : nullptr;
0765         *p = askUserInterface;
0766     }
0767 }
0768 
0769 #include "fileundomanager.moc"
0770 #include "moc_fileundomanager.cpp"
0771 #include "moc_fileundomanager_p.cpp"