| 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 | |