1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
4 SPDX-FileCopyrightText: 2006, 2008 David Faure <faure@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "fileundomanager.h"
10#include "askuseractioninterface.h"
11#include "clipboardupdater_p.h"
12#ifdef WITH_QTDBUS
13#include "fileundomanager_adaptor.h"
14#endif
15#include "fileundomanager_p.h"
16#include "kio_widgets_debug.h"
17#include <job_p.h>
18#include <kdirnotify.h>
19#include <kio/batchrenamejob.h>
20#include <kio/copyjob.h>
21#include <kio/filecopyjob.h>
22#include <kio/jobuidelegate.h>
23#include <kio/mkdirjob.h>
24#include <kio/mkpathjob.h>
25#include <kio/statjob.h>
26
27#include <KJobTrackerInterface>
28#include <KJobWidgets>
29#include <KLocalizedString>
30#include <KMessageBox>
31
32#ifdef WITH_QTDBUS
33#include <QDBusConnection>
34#endif
35
36#include <QDateTime>
37#include <QFileInfo>
38#include <QLocale>
39
40using namespace KIO;
41
42static const char *undoStateToString(UndoState state)
43{
44 static const char *const s_undoStateToString[] = {"MAKINGDIRS", "MOVINGFILES", "STATINGFILE", "MOVINGLINK", "TRASHINGFILES", "REMOVINGDIRS"};
45 return s_undoStateToString[state];
46}
47
48static QDataStream &operator<<(QDataStream &stream, const KIO::BasicOperation &op)
49{
50 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);
51 return stream;
52}
53static QDataStream &operator>>(QDataStream &stream, BasicOperation &op)
54{
55 qint8 type;
56 qint64 mtime;
57 stream >> op.m_valid >> type >> op.m_renamed >> op.m_src >> op.m_dst >> op.m_target >> mtime;
58 op.m_type = static_cast<BasicOperation::Type>(type);
59 op.m_mtime = QDateTime::fromSecsSinceEpoch(secs: mtime, timeZone: QTimeZone::UTC);
60 return stream;
61}
62
63static QDataStream &operator<<(QDataStream &stream, const UndoCommand &cmd)
64{
65 stream << cmd.m_valid << (qint8)cmd.m_type << cmd.m_opQueue << cmd.m_src << cmd.m_dst;
66 return stream;
67}
68
69static QDataStream &operator>>(QDataStream &stream, UndoCommand &cmd)
70{
71 qint8 type;
72 stream >> cmd.m_valid >> type >> cmd.m_opQueue >> cmd.m_src >> cmd.m_dst;
73 cmd.m_type = static_cast<FileUndoManager::CommandType>(type);
74 return stream;
75}
76
77QDebug operator<<(QDebug dbg, const BasicOperation &op)
78{
79 if (op.m_valid) {
80 static const char *s_types[] = {"File", "Link", "Directory"};
81 dbg << "BasicOperation: type" << s_types[op.m_type] << "src" << op.m_src << "dest" << op.m_dst << "target" << op.m_target << "renamed" << op.m_renamed;
82 } else {
83 dbg << "Invalid BasicOperation";
84 }
85 return dbg;
86}
87/*
88 * checklist:
89 * copy dir -> overwrite -> works
90 * move dir -> overwrite -> works
91 * copy dir -> rename -> works
92 * move dir -> rename -> works
93 *
94 * copy dir -> works
95 * move dir -> works
96 *
97 * copy files -> works
98 * move files -> works (TODO: optimize (change FileCopyJob to use the renamed arg for copyingDone)
99 *
100 * copy files -> overwrite -> works (sorry for your overwritten file...)
101 * move files -> overwrite -> works (sorry for your overwritten file...)
102 *
103 * copy files -> rename -> works
104 * move files -> rename -> works
105 *
106 * -> see also fileundomanagertest, which tests some of the above (but not renaming).
107 *
108 */
109
110class KIO::UndoJob : public KIO::Job
111{
112 Q_OBJECT
113public:
114 UndoJob(bool showProgressInfo)
115 : KIO::Job()
116 {
117 if (showProgressInfo) {
118 KIO::getJobTracker()->registerJob(job: this);
119 }
120
121 d_ptr->m_privilegeExecutionEnabled = true;
122 d_ptr->m_operationType = d_ptr->Other;
123 d_ptr->m_title = i18n("Undo Changes");
124 d_ptr->m_message = i18n("Undoing this operation requires root privileges. Do you want to continue?");
125 }
126
127 ~UndoJob() override = default;
128
129 void emitCreatingDir(const QUrl &dir)
130 {
131 Q_EMIT description(job: this, i18n("Creating directory"), field1: qMakePair(i18n("Directory"), value2: dir.toDisplayString()));
132 }
133
134 void emitCopying(const QUrl &src, const QUrl &dst)
135 {
136 Q_EMIT description(job: this, i18n("Copying"), field1: qMakePair(i18n("Source"), value2: src.toDisplayString()), field2: qMakePair(i18n("Destination"), value2: dst.toDisplayString()));
137 }
138
139 void emitMovingOrRenaming(const QUrl &src, const QUrl &dest, FileUndoManager::CommandType cmdType)
140 {
141 static const QString srcMsg(i18nc("The source of a file operation", "Source"));
142 static const QString destMsg(i18nc("The destination of a file operation", "Destination"));
143
144 Q_EMIT description(job: this, //
145 title: cmdType == FileUndoManager::Move ? i18n("Moving") : i18n("Renaming"),
146 field1: {srcMsg, src.toDisplayString()},
147 field2: {destMsg, dest.toDisplayString()});
148 }
149
150 void emitTrashing()
151 {
152 Q_EMIT description(job: this, i18n("Moving to Trash"));
153 }
154
155 void emitDeleting(const QUrl &url)
156 {
157 Q_EMIT description(job: this, i18n("Deleting"), field1: qMakePair(i18n("File"), value2: url.toDisplayString()));
158 }
159
160 void emitResult()
161 {
162 KIO::Job::emitResult();
163 }
164
165protected:
166 bool doKill() override
167 {
168 FileUndoManager::self()->d->stopUndoOrRedo(step: true);
169 return KIO::Job::doKill();
170 }
171};
172
173CommandRecorder::CommandRecorder(FileUndoManager::CommandType op,
174 const QList<QUrl> &src,
175 const QUrl &dst,
176 std::function<void(UndoCommand)> onFinished,
177 KIO::Job *job)
178 : QObject(job)
179 , m_cmd(op, src, dst, FileUndoManager::self()->newCommandSerialNumber())
180 , m_onFinished(onFinished)
181{
182 connect(sender: job, signal: &KJob::result, context: this, slot: &CommandRecorder::slotResult);
183 if (auto *copyJob = qobject_cast<KIO::CopyJob *>(object: job)) {
184 connect(sender: copyJob, signal: &KIO::CopyJob::copyingDone, context: this, slot: &CommandRecorder::slotCopyingDone);
185 connect(sender: copyJob, signal: &KIO::CopyJob::copyingLinkDone, context: this, slot: &CommandRecorder::slotCopyingLinkDone);
186 } else if (auto *mkpathJob = qobject_cast<KIO::MkpathJob *>(object: job)) {
187 connect(sender: mkpathJob, signal: &KIO::MkpathJob::directoryCreated, context: this, slot: &CommandRecorder::slotDirectoryCreated);
188 } else if (auto *batchRenameJob = qobject_cast<KIO::BatchRenameJob *>(object: job)) {
189 connect(sender: batchRenameJob, signal: &KIO::BatchRenameJob::fileRenamed, context: this, slot: &CommandRecorder::slotBatchRenamingDone);
190 }
191}
192
193void CommandRecorder::slotResult(KJob *job)
194{
195 const int err = job->error();
196 if (err) {
197 if (err != KIO::ERR_USER_CANCELED) {
198 qCDebug(KIO_WIDGETS) << "CommandRecorder::slotResult:" << job->errorString() << " - no undo command will be added";
199 }
200 return;
201 }
202
203 // For CopyJob, don't add an undo command unless the job actually did something,
204 // e.g. if user selected to skip all, there is nothing to undo.
205 // Note: this doesn't apply to other job types, e.g. for Mkdir m_opQueue is
206 // expected to be empty
207 if (qobject_cast<KIO::CopyJob *>(object: job)) {
208 if (!m_cmd.m_opQueue.isEmpty()) {
209 m_onFinished(m_cmd);
210 }
211 return;
212 }
213
214 m_onFinished(m_cmd);
215}
216
217void CommandRecorder::slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &mtime, bool directory, bool renamed)
218{
219 const BasicOperation::Type type = directory ? BasicOperation::Directory : BasicOperation::File;
220 m_cmd.m_opQueue.enqueue(t: BasicOperation(type, renamed, from, to, mtime));
221}
222
223void CommandRecorder::slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to)
224{
225 m_cmd.m_opQueue.enqueue(t: BasicOperation(BasicOperation::Link, false, from, to, {}, target));
226}
227
228void CommandRecorder::slotDirectoryCreated(const QUrl &dir)
229{
230 m_cmd.m_opQueue.enqueue(t: BasicOperation(BasicOperation::Directory, false, QUrl{}, dir, {}));
231}
232
233void CommandRecorder::slotBatchRenamingDone(const QUrl &from, const QUrl &to)
234{
235 m_cmd.m_opQueue.enqueue(t: BasicOperation(BasicOperation::Item, true, from, to, {}));
236}
237
238////
239
240class KIO::FileUndoManagerSingleton
241{
242public:
243 FileUndoManager self;
244};
245Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager)
246
247FileUndoManager *FileUndoManager::self()
248{
249 return &globalFileUndoManager()->self;
250}
251
252// m_nextCommandIndex is initialized to a high number so that konqueror can
253// assign low numbers to closed items loaded "on-demand" from a config file
254// in KonqClosedWindowsManager::readConfig and thus maintaining the real
255// order of the undo items.
256FileUndoManagerPrivate::FileUndoManagerPrivate(FileUndoManager *qq)
257 : m_uiInterface(new FileUndoManager::UiInterface())
258 , m_nextCommandIndex(1000)
259 , q(qq)
260{
261#ifdef WITH_QTDBUS
262 (void)new KIOFileUndoManagerAdaptor(this);
263 const QString dbusPath = QStringLiteral("/FileUndoManager");
264 const QString dbusInterface = QStringLiteral("org.kde.kio.FileUndoManager");
265
266 QDBusConnection dbus = QDBusConnection::sessionBus();
267 dbus.registerObject(path: dbusPath, object: this);
268 dbus.connect(service: QString(), path: dbusPath, interface: dbusInterface, QStringLiteral("lock"), receiver: this, SLOT(slotLock()));
269 dbus.connect(service: QString(), path: dbusPath, interface: dbusInterface, QStringLiteral("pop"), receiver: this, SLOT(slotPopUndoCommand()));
270 dbus.connect(service: QString(), path: dbusPath, interface: dbusInterface, QStringLiteral("push"), receiver: this, SLOT(slotPushUndoCommand(QByteArray)));
271 dbus.connect(service: QString(), path: dbusPath, interface: dbusInterface, QStringLiteral("unlock"), receiver: this, SLOT(slotUnlock()));
272#endif
273}
274
275FileUndoManager::FileUndoManager()
276 : d(new FileUndoManagerPrivate(this))
277{
278}
279
280FileUndoManager::~FileUndoManager() = default;
281
282void FileUndoManager::recordJob(CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job)
283{
284 // This records what the job does and calls addUndoCommand when done
285 auto onFinished = [this](UndoCommand cmd) {
286 d->addUndoCommand(cmd);
287 };
288 (void)new CommandRecorder(op, src, dst, onFinished, job);
289 Q_EMIT jobRecordingStarted(op);
290}
291
292void FileUndoManager::recordCopyJob(KIO::CopyJob *copyJob)
293{
294 CommandType commandType;
295 switch (copyJob->operationMode()) {
296 case CopyJob::Copy:
297 commandType = Copy;
298 break;
299 case CopyJob::Move:
300 commandType = Move;
301 break;
302 case CopyJob::Link:
303 commandType = Link;
304 break;
305 default:
306 Q_UNREACHABLE();
307 }
308 recordJob(op: commandType, src: copyJob->srcUrls(), dst: copyJob->destUrl(), job: copyJob);
309}
310
311void FileUndoManagerPrivate::addUndoCommand(const UndoCommand &cmd)
312{
313 clearRedoStack();
314 pushUndoCommand(cmd);
315 Q_EMIT q->jobRecordingFinished(op: cmd.m_type);
316}
317
318bool FileUndoManager::isUndoAvailable() const
319{
320 return !d->m_undoCommands.isEmpty() && !d->m_lock;
321}
322
323bool FileUndoManager::isRedoAvailable() const
324{
325 return !d->m_redoCommands.isEmpty() && !d->m_lock;
326}
327
328QString FileUndoManager::undoText() const
329{
330 if (d->m_undoCommands.isEmpty()) {
331 return i18n("Und&o");
332 }
333
334 FileUndoManager::CommandType t = d->m_undoCommands.top().m_type;
335 switch (t) {
336 case FileUndoManager::Copy:
337 return i18n("Und&o: Copy");
338 case FileUndoManager::Link:
339 return i18n("Und&o: Link");
340 case FileUndoManager::Move:
341 return i18n("Und&o: Move");
342 case FileUndoManager::Rename:
343 return i18n("Und&o: Rename");
344 case FileUndoManager::Trash:
345 return i18n("Und&o: Trash");
346 case FileUndoManager::Mkdir:
347 return i18n("Und&o: Create Folder");
348 case FileUndoManager::Mkpath:
349 return i18n("Und&o: Create Folder(s)");
350 case FileUndoManager::Put:
351 return i18n("Und&o: Create File");
352 case FileUndoManager::BatchRename:
353 return i18n("Und&o: Batch Rename");
354 }
355 /* NOTREACHED */
356 return QString();
357}
358
359QString FileUndoManager::redoText() const
360{
361 if (d->m_redoCommands.isEmpty()) {
362 return i18n("&Redo");
363 }
364
365 FileUndoManager::CommandType t = d->m_redoCommands.top().m_type;
366 switch (t) {
367 case FileUndoManager::Copy:
368 return i18n("&Redo: Copy");
369 case FileUndoManager::Link:
370 return i18n("&Redo: Link");
371 case FileUndoManager::Move:
372 return i18n("&Redo: Move");
373 case FileUndoManager::Rename:
374 return i18n("&Redo: Rename");
375 case FileUndoManager::Trash:
376 return i18n("&Redo: Trash");
377 case FileUndoManager::Mkdir:
378 return i18n("&Redo: Create Folder");
379 case FileUndoManager::Mkpath:
380 return i18n("&Redo: Create Folder(s)");
381 case FileUndoManager::Put:
382 return i18n("&Redo: Create File");
383 case FileUndoManager::BatchRename:
384 return i18n("&Redo: Batch Rename");
385 }
386 /* NOTREACHED */
387 return QString();
388}
389
390quint64 FileUndoManager::newCommandSerialNumber()
391{
392 return ++(d->m_nextCommandIndex);
393}
394
395quint64 FileUndoManager::currentCommandSerialNumber() const
396{
397 if (!d->m_undoCommands.isEmpty()) {
398 const UndoCommand &cmd = d->m_undoCommands.top();
399 Q_ASSERT(cmd.m_valid);
400 return cmd.m_serialNumber;
401 }
402
403 return 0;
404}
405
406void FileUndoManager::undo()
407{
408 Q_ASSERT(!d->m_undoCommands.isEmpty()); // forgot to record before calling undo?
409
410 // Make a copy of the command to undo before slotPopUndoCommand() pops it.
411 UndoCommand cmd = d->m_undoCommands.last();
412 Q_ASSERT(cmd.m_valid);
413 d->m_currentCmd = d->m_cmdToBePushed = cmd;
414
415 d->startUndoOrRedo(redo: false);
416}
417
418void FileUndoManager::redo()
419{
420 Q_ASSERT(!d->m_redoCommands.isEmpty()); // forgot to record before calling redo?
421
422 // Make a copy of the command to redo before slotPopRedoCommand() pops it.
423 UndoCommand cmd = d->m_redoCommands.last();
424 Q_ASSERT(cmd.m_valid);
425 d->m_currentCmd = d->m_cmdToBePushed = cmd;
426
427 d->startUndoOrRedo(redo: true);
428}
429
430void FileUndoManagerPrivate::startUndoOrRedo(bool redo)
431{
432 slotLock();
433 if (redo) {
434 popRedoCommand();
435 } else {
436 slotPopUndoCommand();
437 }
438
439 m_dirCleanupStack.clear();
440 m_dirStack.clear();
441 m_dirsToUpdate.clear();
442
443 m_undoState = MOVINGFILES;
444
445 // Let's have a look at the basic operations we need to undo.
446 auto &opQueue = m_currentCmd.m_opQueue;
447 for (auto it = opQueue.rbegin(); it != opQueue.rend(); ++it) {
448 const BasicOperation::Type type = (*it).m_type;
449 if (type == BasicOperation::Directory && !(*it).m_renamed) {
450 // If any directory has to be created/deleted, we'll start with that
451 m_undoState = MAKINGDIRS;
452 // Collect all the dirs that have to be created in case of a move undo.
453 if (m_currentCmd.isMoveOrRename()) {
454 if (redo) {
455 m_dirCleanupStack.prepend(t: (*it).m_src);
456 } else {
457 m_dirStack.push(t: (*it).m_src);
458 }
459 }
460 // Collect all dirs that have to be deleted
461 // from the destination in both cases (copy and move).
462 if (redo) {
463 m_dirStack.push(t: (*it).m_dst);
464 } else {
465 m_dirCleanupStack.prepend(t: (*it).m_dst);
466 }
467 }
468 }
469 auto isBasicOperation = [](const BasicOperation &op) {
470 return (op.m_type == BasicOperation::Directory && !op.m_renamed);
471 };
472 opQueue.erase(abegin: std::remove_if(first: opQueue.begin(), last: opQueue.end(), pred: isBasicOperation), aend: opQueue.end());
473
474 const FileUndoManager::CommandType commandType = m_currentCmd.m_type;
475 if (commandType == FileUndoManager::Put) {
476 if (redo) {
477 m_cmdToBePushed.m_opQueue.clear();
478 } else {
479 m_fileTrashStack.append(t: m_currentCmd.m_dst);
480 }
481 } else if (commandType == FileUndoManager::Mkdir) {
482 if (redo) {
483 m_undoState = MAKINGDIRS;
484 m_dirStack.push(t: m_currentCmd.m_dst);
485 } else {
486 m_dirCleanupStack.push(t: m_currentCmd.m_dst);
487 }
488 } else if (commandType == FileUndoManager::Trash && redo) {
489 m_fileTrashStack.append(l: m_currentCmd.m_src);
490 m_currentCmd.m_opQueue.clear();
491 }
492
493 qCDebug(KIO_WIDGETS) << "starting with" << undoStateToString(state: m_undoState);
494 m_undoJob = new UndoJob(m_uiInterface->showProgressInfo());
495 auto func = [this, redo]() {
496 processStep(redo);
497 };
498 auto onFinished = [this, redo](KJob *job) {
499 if (!job->error()) {
500 if (redo) {
501 pushUndoCommand(cmd: m_cmdToBePushed);
502 } else {
503 pushRedoCommand(cmd: m_cmdToBePushed);
504 }
505 }
506 };
507 connect(sender: m_undoJob, signal: &KIO::UndoJob::result, context: this, slot&: onFinished);
508 QMetaObject::invokeMethod(object: this, function&: func, type: Qt::QueuedConnection);
509}
510
511void FileUndoManagerPrivate::stopUndoOrRedo(bool step)
512{
513 m_currentCmd.m_opQueue.clear();
514 m_dirCleanupStack.clear();
515 m_fileTrashStack.clear();
516 m_undoState = REMOVINGDIRS;
517 m_undoJob = nullptr;
518
519 if (m_currentJob) {
520 m_currentJob->kill();
521 }
522
523 m_currentJob = nullptr;
524
525 if (step) {
526 processStep(redo: false);
527 }
528}
529
530void FileUndoManagerPrivate::slotUndoResult(KJob *job)
531{
532 m_currentJob = nullptr;
533 if (job->error()) {
534 qWarning() << job->errorString();
535 m_uiInterface->jobError(job: static_cast<KIO::Job *>(job));
536 delete m_undoJob;
537 stopUndoOrRedo(step: false);
538 } else if (m_undoState == STATINGFILE) {
539 const BasicOperation op = m_currentCmd.m_opQueue.head();
540 // qDebug() << "stat result for " << op.m_dst;
541 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
542 const QDateTime mtime = QDateTime::fromSecsSinceEpoch(secs: statJob->statResult().numberValue(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, defaultValue: -1), timeZone: QTimeZone::UTC);
543 if (mtime != op.m_mtime) {
544 qCDebug(KIO_WIDGETS) << op.m_dst << "was modified after being copied. Initial timestamp" << mtime << "now" << op.m_mtime;
545 QDateTime srcTime = op.m_mtime.toLocalTime();
546 QDateTime destTime = mtime.toLocalTime();
547 if (!m_uiInterface->copiedFileWasModified(src: op.m_src, dest: op.m_dst, srcTime, destTime)) {
548 stopUndoOrRedo(step: false);
549 }
550 }
551 } else if (m_undoState == TRASHINGFILES) {
552 Q_ASSERT(m_currentCmd.m_type == FileUndoManager::Put && m_cmdToBePushed.m_src.size() == 1);
553 std::swap(a&: m_cmdToBePushed.m_src.front(), b&: m_cmdToBePushed.m_dst);
554 for (BasicOperation &op : m_cmdToBePushed.m_opQueue) {
555 std::swap(a&: op.m_src, b&: op.m_dst);
556 }
557 }
558
559 processStep(redo: false);
560}
561
562void FileUndoManagerPrivate::slotRedoResult(KJob *job)
563{
564 m_currentJob = nullptr;
565 if (job->error()) {
566 qWarning() << job->errorString();
567 m_uiInterface->jobError(job: static_cast<KIO::Job *>(job));
568 delete m_undoJob;
569 stopUndoOrRedo(step: false);
570 }
571
572 processStep(redo: true);
573}
574
575void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url)
576{
577 if (!m_dirsToUpdate.contains(t: url)) {
578 m_dirsToUpdate.prepend(t: url);
579 }
580}
581
582void FileUndoManagerPrivate::processStep(bool redo)
583{
584 m_currentJob = nullptr;
585
586 if (m_undoState == MAKINGDIRS) {
587 stepMakingDirectories();
588 }
589
590 if (m_undoState == MOVINGFILES || m_undoState == STATINGFILE || m_undoState == MOVINGLINK) {
591 if (redo) {
592 redoStepMovingFiles();
593 } else {
594 undoStepMovingFiles();
595 }
596 }
597
598 if (m_undoState == TRASHINGFILES) {
599 stepTrashingFiles(redo);
600 }
601
602 if (m_undoState == REMOVINGDIRS) {
603 stepRemovingDirectories();
604 }
605
606 if (m_currentJob) {
607 if (m_uiInterface) {
608 KJobWidgets::setWindow(job: m_currentJob, widget: m_uiInterface->parentWidget());
609 }
610 QObject::connect(sender: m_currentJob, signal: &KJob::result, context: this, slot: redo ? &FileUndoManagerPrivate::slotRedoResult : &FileUndoManagerPrivate::slotUndoResult);
611 }
612}
613
614void FileUndoManagerPrivate::stepMakingDirectories()
615{
616 if (!m_dirStack.isEmpty()) {
617 QUrl dir = m_dirStack.pop();
618 // qDebug() << "creatingDir" << dir;
619 m_currentJob = KIO::mkdir(url: dir);
620 m_currentJob->setParentJob(m_undoJob);
621 m_undoJob->emitCreatingDir(dir);
622 } else {
623 m_undoState = MOVINGFILES;
624 }
625}
626
627void FileUndoManagerPrivate::stepTrashingFiles(bool redo)
628{
629 if (!m_fileTrashStack.empty()) {
630 m_currentJob = KIO::trash(src: m_fileTrashStack, flags: KIO::HideProgressInfo);
631 m_currentJob->setParentJob(m_undoJob);
632 auto onFinished = [this](UndoCommand cmd) {
633 m_cmdToBePushed = cmd;
634 };
635 new CommandRecorder(m_currentCmd.m_type, m_fileTrashStack, QUrl(QStringLiteral("trash:/")), onFinished, m_currentJob);
636 connect(sender: m_currentJob, signal: &KJob::result, context: this, slot: redo ? &FileUndoManagerPrivate::slotRedoResult : &FileUndoManagerPrivate::slotUndoResult);
637 m_undoJob->emitTrashing();
638
639 while (!m_fileTrashStack.empty()) {
640 const QUrl url = m_fileTrashStack.pop().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
641 addDirToUpdate(url);
642 }
643 } else {
644 m_undoState = REMOVINGDIRS;
645 }
646}
647
648void FileUndoManagerPrivate::stepRemovingDirectories()
649{
650 if (!m_dirCleanupStack.isEmpty()) {
651 QUrl dir = m_dirCleanupStack.pop();
652 // qDebug() << "rmdir" << dir;
653 m_currentJob = KIO::rmdir(url: dir);
654 m_currentJob->setParentJob(m_undoJob);
655 m_undoJob->emitDeleting(url: dir);
656 addDirToUpdate(url: dir);
657 } else {
658 m_currentCmd.m_valid = false;
659 m_currentJob = nullptr;
660 if (m_undoJob) {
661 // qDebug() << "deleting undojob";
662 m_undoJob->emitResult();
663 m_undoJob = nullptr;
664 }
665#ifdef WITH_QTDBUS
666 for (const QUrl &url : std::as_const(t&: m_dirsToUpdate)) {
667 // qDebug() << "Notifying FilesAdded for " << url;
668 org::kde::KDirNotify::emitFilesAdded(directory: url);
669 }
670#endif
671 slotUnlock();
672 Q_EMIT q->undoJobFinished();
673 }
674}
675
676// Misnamed method: It moves files back, but it also
677// renames directories back, recreates symlinks,
678// deletes copied files, and restores trashed files.
679void FileUndoManagerPrivate::undoStepMovingFiles()
680{
681 if (m_currentCmd.m_opQueue.isEmpty()) {
682 m_undoState = TRASHINGFILES;
683 return;
684 }
685
686 const BasicOperation op = m_currentCmd.m_opQueue.head();
687 Q_ASSERT(op.m_valid);
688 if (op.m_type == BasicOperation::Directory || op.m_type == BasicOperation::Item) {
689 Q_ASSERT(op.m_renamed);
690 // qDebug() << "rename" << op.m_dst << op.m_src;
691 m_currentJob = KIO::rename(src: op.m_dst, dest: op.m_src, flags: KIO::HideProgressInfo);
692 m_undoJob->emitMovingOrRenaming(src: op.m_dst, dest: op.m_src, cmdType: m_currentCmd.m_type);
693 } else if (op.m_type == BasicOperation::Link) {
694 if (m_currentCmd.isMoveOrRename() && m_undoState != MOVINGLINK) { // Moving or renaming a link is done in two steps
695 m_currentJob = KIO::symlink(target: op.m_target, dest: op.m_src, flags: KIO::HideProgressInfo);
696 m_undoState = MOVINGLINK; // temporarily
697 return;
698 } else {
699 m_currentJob = KIO::file_delete(src: op.m_dst);
700 m_undoState = MOVINGFILES;
701 }
702 } else if (m_currentCmd.m_type == FileUndoManager::Copy) {
703 if (m_undoState == MOVINGFILES) { // dest not stat'ed yet
704 // Before we delete op.m_dst, let's check if it was modified (#20532)
705 // qDebug() << "stat" << op.m_dst;
706 m_currentJob = KIO::stat(url: op.m_dst, flags: KIO::HideProgressInfo);
707 m_undoState = STATINGFILE; // temporarily
708 return; // no pop() yet, we'll finish the work in slotResult
709 } else { // dest was stat'ed, and the deletion was approved in slotResult
710 m_currentJob = KIO::file_delete(src: op.m_dst, flags: KIO::HideProgressInfo);
711 m_undoJob->emitDeleting(url: op.m_dst);
712 m_undoState = MOVINGFILES;
713 }
714 } else if (m_currentCmd.isMoveOrRename() || m_currentCmd.m_type == FileUndoManager::Trash) {
715 m_currentJob = KIO::file_move(src: op.m_dst, dest: op.m_src, permissions: -1, flags: KIO::HideProgressInfo);
716 m_currentJob->uiDelegateExtension()->createClipboardUpdater(job: m_currentJob, mode: JobUiDelegateExtension::UpdateContent);
717 m_undoJob->emitMovingOrRenaming(src: op.m_dst, dest: op.m_src, cmdType: m_currentCmd.m_type);
718 }
719
720 if (m_currentJob) {
721 m_currentJob->setParentJob(m_undoJob);
722 }
723
724 m_currentCmd.m_opQueue.dequeue();
725 // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification
726 // So we need to do it ourselves (but schedule it to the end of the undo, to compress them)
727 QUrl url = op.m_dst.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
728 addDirToUpdate(url);
729
730 url = op.m_src.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
731 addDirToUpdate(url);
732}
733
734void FileUndoManagerPrivate::redoStepMovingFiles()
735{
736 if (m_currentCmd.m_opQueue.isEmpty()) {
737 m_undoState = TRASHINGFILES;
738 return;
739 }
740
741 const BasicOperation op = m_currentCmd.m_opQueue.head();
742 Q_ASSERT(op.m_valid);
743 if (op.m_type == BasicOperation::Directory || op.m_type == BasicOperation::Item) {
744 Q_ASSERT(op.m_renamed);
745 // qDebug() << "rename" << op.m_dst << op.m_src;
746 m_currentJob = KIO::rename(src: op.m_src, dest: op.m_dst, flags: KIO::HideProgressInfo);
747 m_undoJob->emitMovingOrRenaming(src: op.m_src, dest: op.m_dst, cmdType: m_currentCmd.m_type);
748 } else if (op.m_type == BasicOperation::Link) {
749 if (m_currentCmd.isMoveOrRename() && m_undoState != MOVINGLINK) { // Moving or renaming a link is done in two steps
750 m_currentJob = KIO::file_delete(src: op.m_src);
751 m_undoState = MOVINGLINK; // temporarily
752 return;
753 } else {
754 m_currentJob = KIO::symlink(target: op.m_target, dest: op.m_dst);
755 m_undoState = MOVINGFILES;
756 }
757 } else if (m_currentCmd.m_type == FileUndoManager::Copy) {
758 m_currentJob = KIO::file_copy(src: op.m_src, dest: op.m_dst, permissions: -1, flags: KIO::HideProgressInfo);
759 m_undoJob->emitCopying(src: op.m_src, dst: op.m_dst);
760 } else if (m_currentCmd.isMoveOrRename() || m_currentCmd.m_type == FileUndoManager::Put) {
761 m_currentJob = KIO::file_move(src: op.m_src, dest: op.m_dst, permissions: -1, flags: KIO::HideProgressInfo);
762 m_currentJob->uiDelegateExtension()->createClipboardUpdater(job: m_currentJob, mode: JobUiDelegateExtension::UpdateContent);
763 m_undoJob->emitMovingOrRenaming(src: op.m_src, dest: op.m_dst, cmdType: m_currentCmd.m_type);
764 }
765
766 if (m_currentJob) {
767 m_currentJob->setParentJob(m_undoJob);
768 }
769
770 m_currentCmd.m_opQueue.dequeue();
771 // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification
772 // So we need to do it ourselves (but schedule it to the end of the undo, to compress them)
773 QUrl url = op.m_dst.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
774 addDirToUpdate(url);
775
776 url = op.m_src.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
777 addDirToUpdate(url);
778}
779
780// const ref doesn't work due to QDataStream
781void FileUndoManagerPrivate::slotPushUndoCommand(QByteArray data)
782{
783 QDataStream strm(&data, QIODevice::ReadOnly);
784 UndoCommand cmd;
785 strm >> cmd;
786 clearRedoStack();
787 pushUndoCommand(cmd);
788}
789
790void FileUndoManagerPrivate::pushUndoCommand(const UndoCommand &cmd)
791{
792 m_undoCommands.push(t: cmd);
793 if (m_undoCommands.size() == 1 && !m_lock) {
794 Q_EMIT q->undoAvailable(avail: true);
795 }
796 Q_EMIT q->undoTextChanged(text: q->undoText());
797}
798
799void FileUndoManagerPrivate::slotPopUndoCommand()
800{
801 m_undoCommands.pop();
802 if (m_undoCommands.empty() && !m_lock) {
803 Q_EMIT q->undoAvailable(avail: false);
804 }
805 Q_EMIT q->undoTextChanged(text: q->undoText());
806}
807
808void FileUndoManagerPrivate::slotLock()
809{
810 // Q_ASSERT(!m_lock);
811 if (q->isUndoAvailable()) {
812 Q_EMIT q->undoAvailable(avail: false);
813 }
814 if (q->isRedoAvailable()) {
815 Q_EMIT q->redoAvailable(avail: false);
816 }
817 m_lock = true;
818}
819
820void FileUndoManagerPrivate::slotUnlock()
821{
822 // Q_ASSERT(m_lock);
823 m_lock = false;
824 if (q->isUndoAvailable()) {
825 Q_EMIT q->undoAvailable(avail: true);
826 }
827 if (q->isRedoAvailable()) {
828 Q_EMIT q->redoAvailable(avail: true);
829 }
830}
831
832void FileUndoManagerPrivate::pushRedoCommand(const UndoCommand &cmd)
833{
834 m_redoCommands.push(t: cmd);
835 if (m_redoCommands.size() == 1 && !m_lock) {
836 Q_EMIT q->redoAvailable(avail: true);
837 }
838 Q_EMIT q->redoTextChanged(text: q->redoText());
839}
840
841void FileUndoManagerPrivate::popRedoCommand()
842{
843 m_redoCommands.pop();
844 if (m_redoCommands.empty() && !m_lock) {
845 Q_EMIT q->redoAvailable(avail: false);
846 }
847 Q_EMIT q->redoTextChanged(text: q->redoText());
848}
849
850void FileUndoManagerPrivate::clearRedoStack()
851{
852 bool wasEmpty = m_redoCommands.empty();
853 m_redoCommands.clear();
854 if (!wasEmpty && !m_lock) {
855 Q_EMIT q->redoAvailable(avail: false);
856 }
857 if (!wasEmpty) {
858 Q_EMIT q->redoTextChanged(text: q->redoText());
859 }
860}
861
862QByteArray FileUndoManagerPrivate::get() const
863{
864 QByteArray data;
865 QDataStream stream(&data, QIODevice::WriteOnly);
866 stream << m_undoCommands;
867 return data;
868}
869
870void FileUndoManager::setUiInterface(UiInterface *ui)
871{
872 d->m_uiInterface.reset(p: ui);
873}
874
875FileUndoManager::UiInterface *FileUndoManager::uiInterface() const
876{
877 return d->m_uiInterface.get();
878}
879
880////
881
882class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate
883{
884public:
885 QPointer<QWidget> m_parentWidget;
886 bool m_showProgressInfo = true;
887};
888
889FileUndoManager::UiInterface::UiInterface()
890 : d(new UiInterfacePrivate)
891{
892}
893
894FileUndoManager::UiInterface::~UiInterface() = default;
895
896void FileUndoManager::UiInterface::jobError(KIO::Job *job)
897{
898 job->uiDelegate()->showErrorMessage();
899}
900
901bool FileUndoManager::UiInterface::copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime)
902{
903 Q_UNUSED(srcTime); // not sure it should appear in the msgbox
904 // Possible improvement: only show the time if date is today
905 const QString timeStr = QLocale().toString(dateTime: destTime, format: QLocale::ShortFormat);
906 const QString msg = i18n(
907 "The file %1 was copied from %2, but since then it has apparently been modified at %3.\n"
908 "Undoing the copy will delete the file, and all modifications will be lost.\n"
909 "Are you sure you want to delete %4?",
910 dest.toDisplayString(QUrl::PreferLocalFile),
911 src.toDisplayString(QUrl::PreferLocalFile),
912 timeStr,
913 dest.toDisplayString(QUrl::PreferLocalFile));
914
915 const auto result = KMessageBox::warningContinueCancel(parent: d->m_parentWidget,
916 text: msg,
917 i18n("Undo File Copy Confirmation"),
918 buttonContinue: KStandardGuiItem::cont(),
919 buttonCancel: KStandardGuiItem::cancel(),
920 dontAskAgainName: QString(),
921 options: KMessageBox::Options(KMessageBox::Notify) | KMessageBox::Dangerous);
922 return result == KMessageBox::Continue;
923}
924
925QWidget *FileUndoManager::UiInterface::parentWidget() const
926{
927 return d->m_parentWidget;
928}
929
930void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget)
931{
932 d->m_parentWidget = parentWidget;
933}
934
935void FileUndoManager::UiInterface::setShowProgressInfo(bool b)
936{
937 d->m_showProgressInfo = b;
938}
939
940bool FileUndoManager::UiInterface::showProgressInfo() const
941{
942 return d->m_showProgressInfo;
943}
944
945void FileUndoManager::UiInterface::virtual_hook(int id, void *data)
946{
947 if (id == HookGetAskUserActionInterface) {
948 auto *p = static_cast<AskUserActionInterface **>(data);
949 static KJobUiDelegate *delegate = KIO::createDefaultJobUiDelegate();
950 static auto *askUserInterface = delegate ? delegate->findChild<AskUserActionInterface *>(aName: QString(), options: Qt::FindDirectChildrenOnly) : nullptr;
951 *p = askUserInterface;
952 }
953}
954
955#include "fileundomanager.moc"
956#include "moc_fileundomanager.cpp"
957#include "moc_fileundomanager_p.cpp"
958

source code of kio/src/widgets/fileundomanager.cpp