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

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