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 | |
35 | using namespace KIO; |
36 | |
37 | static 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 | |
43 | static 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 | } |
48 | static 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 | |
58 | static 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 | |
64 | static 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 | |
72 | QDebug 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 | |
105 | class KIO::UndoJob : public KIO::Job |
106 | { |
107 | Q_OBJECT |
108 | public: |
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 | |
156 | CommandRecorder::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 | |
171 | void 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 | |
195 | void 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 | |
201 | void 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 | |
206 | void CommandRecorder::slotDirectoryCreated(const QUrl &dir) |
207 | { |
208 | m_cmd.m_opQueue.enqueue(t: BasicOperation(BasicOperation::Directory, false, QUrl{}, dir, {})); |
209 | } |
210 | |
211 | void 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 | |
218 | class KIO::FileUndoManagerSingleton |
219 | { |
220 | public: |
221 | FileUndoManager self; |
222 | }; |
223 | Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager) |
224 | |
225 | FileUndoManager *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. |
234 | FileUndoManagerPrivate::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 | |
253 | FileUndoManager::FileUndoManager() |
254 | : d(new FileUndoManagerPrivate(this)) |
255 | { |
256 | } |
257 | |
258 | FileUndoManager::~FileUndoManager() = default; |
259 | |
260 | void 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 | |
267 | void 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 | |
286 | void FileUndoManagerPrivate::addCommand(const UndoCommand &cmd) |
287 | { |
288 | pushCommand(cmd); |
289 | Q_EMIT q->jobRecordingFinished(op: cmd.m_type); |
290 | } |
291 | |
292 | bool FileUndoManager::isUndoAvailable() const |
293 | { |
294 | return !d->m_commands.isEmpty() && !d->m_lock; |
295 | } |
296 | |
297 | QString 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 | |
328 | quint64 FileUndoManager::newCommandSerialNumber() |
329 | { |
330 | return ++(d->m_nextCommandIndex); |
331 | } |
332 | |
333 | quint64 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 | |
344 | void 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 | |
399 | void 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 | |
447 | void 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 | |
466 | void 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 | |
492 | void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url) |
493 | { |
494 | if (!m_dirsToUpdate.contains(t: url)) { |
495 | m_dirsToUpdate.prepend(t: url); |
496 | } |
497 | } |
498 | |
499 | void 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 | |
527 | void 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. |
543 | void 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 | |
592 | void 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 | |
613 | void 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 |
640 | void FileUndoManagerPrivate::slotPush(QByteArray data) |
641 | { |
642 | QDataStream strm(&data, QIODevice::ReadOnly); |
643 | UndoCommand cmd; |
644 | strm >> cmd; |
645 | pushCommand(cmd); |
646 | } |
647 | |
648 | void 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 | |
655 | void 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 | |
662 | void FileUndoManagerPrivate::slotLock() |
663 | { |
664 | // Q_ASSERT(!m_lock); |
665 | m_lock = true; |
666 | Q_EMIT q->undoAvailable(avail: q->isUndoAvailable()); |
667 | } |
668 | |
669 | void FileUndoManagerPrivate::slotUnlock() |
670 | { |
671 | // Q_ASSERT(m_lock); |
672 | m_lock = false; |
673 | Q_EMIT q->undoAvailable(avail: q->isUndoAvailable()); |
674 | } |
675 | |
676 | QByteArray FileUndoManagerPrivate::get() const |
677 | { |
678 | QByteArray data; |
679 | QDataStream stream(&data, QIODevice::WriteOnly); |
680 | stream << m_commands; |
681 | return data; |
682 | } |
683 | |
684 | void FileUndoManager::setUiInterface(UiInterface *ui) |
685 | { |
686 | d->m_uiInterface.reset(p: ui); |
687 | } |
688 | |
689 | FileUndoManager::UiInterface *FileUndoManager::uiInterface() const |
690 | { |
691 | return d->m_uiInterface.get(); |
692 | } |
693 | |
694 | //// |
695 | |
696 | class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate |
697 | { |
698 | public: |
699 | QPointer<QWidget> m_parentWidget; |
700 | bool m_showProgressInfo = true; |
701 | }; |
702 | |
703 | FileUndoManager::UiInterface::UiInterface() |
704 | : d(new UiInterfacePrivate) |
705 | { |
706 | } |
707 | |
708 | FileUndoManager::UiInterface::~UiInterface() = default; |
709 | |
710 | void FileUndoManager::UiInterface::jobError(KIO::Job *job) |
711 | { |
712 | job->uiDelegate()->showErrorMessage(); |
713 | } |
714 | |
715 | bool 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 | |
739 | QWidget *FileUndoManager::UiInterface::parentWidget() const |
740 | { |
741 | return d->m_parentWidget; |
742 | } |
743 | |
744 | void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget) |
745 | { |
746 | d->m_parentWidget = parentWidget; |
747 | } |
748 | |
749 | void FileUndoManager::UiInterface::setShowProgressInfo(bool b) |
750 | { |
751 | d->m_showProgressInfo = b; |
752 | } |
753 | |
754 | bool FileUndoManager::UiInterface::showProgressInfo() const |
755 | { |
756 | return d->m_showProgressInfo; |
757 | } |
758 | |
759 | void 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 | |