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 | |
40 | using namespace KIO; |
41 | |
42 | static 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 | |
48 | static 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 | } |
53 | static 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 | |
63 | static 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 | |
69 | static 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 | |
77 | QDebug 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 | |
110 | class KIO::UndoJob : public KIO::Job |
111 | { |
112 | Q_OBJECT |
113 | public: |
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 | |
165 | protected: |
166 | bool doKill() override |
167 | { |
168 | FileUndoManager::self()->d->stopUndoOrRedo(step: true); |
169 | return KIO::Job::doKill(); |
170 | } |
171 | }; |
172 | |
173 | CommandRecorder::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 | |
193 | void 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 | |
217 | void 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 | |
223 | void 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 | |
228 | void CommandRecorder::slotDirectoryCreated(const QUrl &dir) |
229 | { |
230 | m_cmd.m_opQueue.enqueue(t: BasicOperation(BasicOperation::Directory, false, QUrl{}, dir, {})); |
231 | } |
232 | |
233 | void 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 | |
240 | class KIO::FileUndoManagerSingleton |
241 | { |
242 | public: |
243 | FileUndoManager self; |
244 | }; |
245 | Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager) |
246 | |
247 | FileUndoManager *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. |
256 | FileUndoManagerPrivate::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 | |
275 | FileUndoManager::FileUndoManager() |
276 | : d(new FileUndoManagerPrivate(this)) |
277 | { |
278 | } |
279 | |
280 | FileUndoManager::~FileUndoManager() = default; |
281 | |
282 | void 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 | |
292 | void 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 | |
311 | void FileUndoManagerPrivate::addUndoCommand(const UndoCommand &cmd) |
312 | { |
313 | clearRedoStack(); |
314 | pushUndoCommand(cmd); |
315 | Q_EMIT q->jobRecordingFinished(op: cmd.m_type); |
316 | } |
317 | |
318 | bool FileUndoManager::isUndoAvailable() const |
319 | { |
320 | return !d->m_undoCommands.isEmpty() && !d->m_lock; |
321 | } |
322 | |
323 | bool FileUndoManager::isRedoAvailable() const |
324 | { |
325 | return !d->m_redoCommands.isEmpty() && !d->m_lock; |
326 | } |
327 | |
328 | QString 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 | |
359 | QString 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 | |
390 | quint64 FileUndoManager::newCommandSerialNumber() |
391 | { |
392 | return ++(d->m_nextCommandIndex); |
393 | } |
394 | |
395 | quint64 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 | |
406 | void 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 | |
418 | void 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 | |
430 | void 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 | |
511 | void 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 | |
530 | void 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 | |
562 | void 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 | |
575 | void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url) |
576 | { |
577 | if (!m_dirsToUpdate.contains(t: url)) { |
578 | m_dirsToUpdate.prepend(t: url); |
579 | } |
580 | } |
581 | |
582 | void 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 | |
614 | void 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 | |
627 | void 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 | |
648 | void 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. |
679 | void 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 | |
734 | void 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 |
781 | void FileUndoManagerPrivate::slotPushUndoCommand(QByteArray data) |
782 | { |
783 | QDataStream strm(&data, QIODevice::ReadOnly); |
784 | UndoCommand cmd; |
785 | strm >> cmd; |
786 | clearRedoStack(); |
787 | pushUndoCommand(cmd); |
788 | } |
789 | |
790 | void 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 | |
799 | void 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 | |
808 | void 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 | |
820 | void 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 | |
832 | void 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 | |
841 | void 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 | |
850 | void 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 | |
862 | QByteArray FileUndoManagerPrivate::get() const |
863 | { |
864 | QByteArray data; |
865 | QDataStream stream(&data, QIODevice::WriteOnly); |
866 | stream << m_undoCommands; |
867 | return data; |
868 | } |
869 | |
870 | void FileUndoManager::setUiInterface(UiInterface *ui) |
871 | { |
872 | d->m_uiInterface.reset(p: ui); |
873 | } |
874 | |
875 | FileUndoManager::UiInterface *FileUndoManager::uiInterface() const |
876 | { |
877 | return d->m_uiInterface.get(); |
878 | } |
879 | |
880 | //// |
881 | |
882 | class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate |
883 | { |
884 | public: |
885 | QPointer<QWidget> m_parentWidget; |
886 | bool m_showProgressInfo = true; |
887 | }; |
888 | |
889 | FileUndoManager::UiInterface::UiInterface() |
890 | : d(new UiInterfacePrivate) |
891 | { |
892 | } |
893 | |
894 | FileUndoManager::UiInterface::~UiInterface() = default; |
895 | |
896 | void FileUndoManager::UiInterface::jobError(KIO::Job *job) |
897 | { |
898 | job->uiDelegate()->showErrorMessage(); |
899 | } |
900 | |
901 | bool 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 | |
925 | QWidget *FileUndoManager::UiInterface::parentWidget() const |
926 | { |
927 | return d->m_parentWidget; |
928 | } |
929 | |
930 | void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget) |
931 | { |
932 | d->m_parentWidget = parentWidget; |
933 | } |
934 | |
935 | void FileUndoManager::UiInterface::setShowProgressInfo(bool b) |
936 | { |
937 | d->m_showProgressInfo = b; |
938 | } |
939 | |
940 | bool FileUndoManager::UiInterface::showProgressInfo() const |
941 | { |
942 | return d->m_showProgressInfo; |
943 | } |
944 | |
945 | void 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 | |