File indexing completed on 2025-02-16 10:02:11
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org> 0004 SPDX-FileCopyrightText: 2006, 2008 David Faure <faure@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "fileundomanager.h" 0010 #include "askuseractioninterface.h" 0011 #include "clipboardupdater_p.h" 0012 #include "fileundomanager_adaptor.h" 0013 #include "fileundomanager_p.h" 0014 #include "kio_widgets_debug.h" 0015 #include <job_p.h> 0016 #include <kdirnotify.h> 0017 #include <kio/batchrenamejob.h> 0018 #include <kio/copyjob.h> 0019 #include <kio/filecopyjob.h> 0020 #include <kio/jobuidelegate.h> 0021 #include <kio/mkdirjob.h> 0022 #include <kio/mkpathjob.h> 0023 #include <kio/statjob.h> 0024 0025 #include <KJobTrackerInterface> 0026 #include <KJobWidgets> 0027 #include <KLocalizedString> 0028 #include <KMessageBox> 0029 0030 #include <QDBusConnection> 0031 #include <QDateTime> 0032 #include <QFileInfo> 0033 #include <QLocale> 0034 0035 using namespace KIO; 0036 0037 static const char *undoStateToString(UndoState state) 0038 { 0039 static const char *const s_undoStateToString[] = {"MAKINGDIRS", "MOVINGFILES", "STATINGFILE", "REMOVINGDIRS", "REMOVINGLINKS"}; 0040 return s_undoStateToString[state]; 0041 } 0042 0043 static QDataStream &operator<<(QDataStream &stream, const KIO::BasicOperation &op) 0044 { 0045 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); 0046 return stream; 0047 } 0048 static QDataStream &operator>>(QDataStream &stream, BasicOperation &op) 0049 { 0050 qint8 type; 0051 qint64 mtime; 0052 stream >> op.m_valid >> type >> op.m_renamed >> op.m_src >> op.m_dst >> op.m_target >> mtime; 0053 op.m_type = static_cast<BasicOperation::Type>(type); 0054 op.m_mtime = QDateTime::fromSecsSinceEpoch(mtime, Qt::UTC); 0055 return stream; 0056 } 0057 0058 static QDataStream &operator<<(QDataStream &stream, const UndoCommand &cmd) 0059 { 0060 stream << cmd.m_valid << (qint8)cmd.m_type << cmd.m_opQueue << cmd.m_src << cmd.m_dst; 0061 return stream; 0062 } 0063 0064 static QDataStream &operator>>(QDataStream &stream, UndoCommand &cmd) 0065 { 0066 qint8 type; 0067 stream >> cmd.m_valid >> type >> cmd.m_opQueue >> cmd.m_src >> cmd.m_dst; 0068 cmd.m_type = static_cast<FileUndoManager::CommandType>(type); 0069 return stream; 0070 } 0071 0072 QDebug operator<<(QDebug dbg, const BasicOperation &op) 0073 { 0074 if (op.m_valid) { 0075 static const char *s_types[] = {"File", "Link", "Directory"}; 0076 dbg << "BasicOperation: type" << s_types[op.m_type] << "src" << op.m_src << "dest" << op.m_dst << "target" << op.m_target << "renamed" << op.m_renamed; 0077 } else { 0078 dbg << "Invalid BasicOperation"; 0079 } 0080 return dbg; 0081 } 0082 /** 0083 * checklist: 0084 * copy dir -> overwrite -> works 0085 * move dir -> overwrite -> works 0086 * copy dir -> rename -> works 0087 * move dir -> rename -> works 0088 * 0089 * copy dir -> works 0090 * move dir -> works 0091 * 0092 * copy files -> works 0093 * move files -> works (TODO: optimize (change FileCopyJob to use the renamed arg for copyingDone) 0094 * 0095 * copy files -> overwrite -> works (sorry for your overwritten file...) 0096 * move files -> overwrite -> works (sorry for your overwritten file...) 0097 * 0098 * copy files -> rename -> works 0099 * move files -> rename -> works 0100 * 0101 * -> see also fileundomanagertest, which tests some of the above (but not renaming). 0102 * 0103 */ 0104 0105 class KIO::UndoJob : public KIO::Job 0106 { 0107 Q_OBJECT 0108 public: 0109 UndoJob(bool showProgressInfo) 0110 : KIO::Job() 0111 { 0112 if (showProgressInfo) { 0113 KIO::getJobTracker()->registerJob(this); 0114 } 0115 0116 d_ptr->m_privilegeExecutionEnabled = true; 0117 d_ptr->m_operationType = d_ptr->Other; 0118 d_ptr->m_title = i18n("Undo Changes"); 0119 d_ptr->m_message = i18n("Undoing this operation requires root privileges. Do you want to continue?"); 0120 } 0121 0122 ~UndoJob() override = default; 0123 0124 virtual void kill(bool) // TODO should be doKill 0125 { 0126 FileUndoManager::self()->d->stopUndo(true); 0127 KIO::Job::doKill(); 0128 } 0129 0130 void emitCreatingDir(const QUrl &dir) 0131 { 0132 Q_EMIT description(this, i18n("Creating directory"), qMakePair(i18n("Directory"), dir.toDisplayString())); 0133 } 0134 0135 void emitMovingOrRenaming(const QUrl &src, const QUrl &dest, FileUndoManager::CommandType cmdType) 0136 { 0137 static const QString srcMsg(i18nc("The source of a file operation", "Source")); 0138 static const QString destMsg(i18nc("The destination of a file operation", "Destination")); 0139 0140 Q_EMIT description(this, // 0141 cmdType == FileUndoManager::Move ? i18n("Moving") : i18n("Renaming"), 0142 {srcMsg, src.toDisplayString()}, 0143 {destMsg, dest.toDisplayString()}); 0144 } 0145 0146 void emitDeleting(const QUrl &url) 0147 { 0148 Q_EMIT description(this, i18n("Deleting"), qMakePair(i18n("File"), url.toDisplayString())); 0149 } 0150 void emitResult() 0151 { 0152 KIO::Job::emitResult(); 0153 } 0154 }; 0155 0156 CommandRecorder::CommandRecorder(FileUndoManager::CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job) 0157 : QObject(job) 0158 , m_cmd(op, src, dst, FileUndoManager::self()->newCommandSerialNumber()) 0159 { 0160 connect(job, &KJob::result, this, &CommandRecorder::slotResult); 0161 if (auto *copyJob = qobject_cast<KIO::CopyJob *>(job)) { 0162 connect(copyJob, &KIO::CopyJob::copyingDone, this, &CommandRecorder::slotCopyingDone); 0163 connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, &CommandRecorder::slotCopyingLinkDone); 0164 } else if (auto *mkpathJob = qobject_cast<KIO::MkpathJob *>(job)) { 0165 connect(mkpathJob, &KIO::MkpathJob::directoryCreated, this, &CommandRecorder::slotDirectoryCreated); 0166 } else if (auto *batchRenameJob = qobject_cast<KIO::BatchRenameJob *>(job)) { 0167 connect(batchRenameJob, &KIO::BatchRenameJob::fileRenamed, this, &CommandRecorder::slotBatchRenamingDone); 0168 } 0169 } 0170 0171 void CommandRecorder::slotResult(KJob *job) 0172 { 0173 const int err = job->error(); 0174 if (err) { 0175 if (err != KIO::ERR_USER_CANCELED) { 0176 qCDebug(KIO_WIDGETS) << "CommandRecorder::slotResult:" << job->errorString() << " - no undo command will be added"; 0177 } 0178 return; 0179 } 0180 0181 // For CopyJob, don't add an undo command unless the job actually did something, 0182 // e.g. if user selected to skip all, there is nothing to undo. 0183 // Note: this doesn't apply to other job types, e.g. for Mkdir m_opQueue is 0184 // expected to be empty 0185 if (qobject_cast<KIO::CopyJob *>(job)) { 0186 if (!m_cmd.m_opQueue.isEmpty()) { 0187 FileUndoManager::self()->d->addCommand(m_cmd); 0188 } 0189 return; 0190 } 0191 0192 FileUndoManager::self()->d->addCommand(m_cmd); 0193 } 0194 0195 void CommandRecorder::slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &mtime, bool directory, bool renamed) 0196 { 0197 const BasicOperation::Type type = directory ? BasicOperation::Directory : BasicOperation::File; 0198 m_cmd.m_opQueue.enqueue(BasicOperation(type, renamed, from, to, mtime)); 0199 } 0200 0201 void CommandRecorder::slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to) 0202 { 0203 m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Link, false, from, to, {}, target)); 0204 } 0205 0206 void CommandRecorder::slotDirectoryCreated(const QUrl &dir) 0207 { 0208 m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Directory, false, QUrl{}, dir, {})); 0209 } 0210 0211 void CommandRecorder::slotBatchRenamingDone(const QUrl &from, const QUrl &to) 0212 { 0213 m_cmd.m_opQueue.enqueue(BasicOperation(BasicOperation::Item, true, from, to, {})); 0214 } 0215 0216 //// 0217 0218 class KIO::FileUndoManagerSingleton 0219 { 0220 public: 0221 FileUndoManager self; 0222 }; 0223 Q_GLOBAL_STATIC(KIO::FileUndoManagerSingleton, globalFileUndoManager) 0224 0225 FileUndoManager *FileUndoManager::self() 0226 { 0227 return &globalFileUndoManager()->self; 0228 } 0229 0230 // m_nextCommandIndex is initialized to a high number so that konqueror can 0231 // assign low numbers to closed items loaded "on-demand" from a config file 0232 // in KonqClosedWindowsManager::readConfig and thus maintaining the real 0233 // order of the undo items. 0234 FileUndoManagerPrivate::FileUndoManagerPrivate(FileUndoManager *qq) 0235 : m_uiInterface(new FileUndoManager::UiInterface()) 0236 , m_nextCommandIndex(1000) 0237 , q(qq) 0238 { 0239 (void)new KIOFileUndoManagerAdaptor(this); 0240 const QString dbusPath = QStringLiteral("/FileUndoManager"); 0241 const QString dbusInterface = QStringLiteral("org.kde.kio.FileUndoManager"); 0242 0243 QDBusConnection dbus = QDBusConnection::sessionBus(); 0244 dbus.registerObject(dbusPath, this); 0245 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("lock"), this, SLOT(slotLock())); 0246 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("pop"), this, SLOT(slotPop())); 0247 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("push"), this, SLOT(slotPush(QByteArray))); 0248 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("unlock"), this, SLOT(slotUnlock())); 0249 } 0250 0251 FileUndoManager::FileUndoManager() 0252 : d(new FileUndoManagerPrivate(this)) 0253 { 0254 } 0255 0256 FileUndoManager::~FileUndoManager() = default; 0257 0258 void FileUndoManager::recordJob(CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job) 0259 { 0260 // This records what the job does and calls addCommand when done 0261 (void)new CommandRecorder(op, src, dst, job); 0262 Q_EMIT jobRecordingStarted(op); 0263 } 0264 0265 void FileUndoManager::recordCopyJob(KIO::CopyJob *copyJob) 0266 { 0267 CommandType commandType; 0268 switch (copyJob->operationMode()) { 0269 case CopyJob::Copy: 0270 commandType = Copy; 0271 break; 0272 case CopyJob::Move: 0273 commandType = Move; 0274 break; 0275 case CopyJob::Link: 0276 commandType = Link; 0277 break; 0278 default: 0279 Q_UNREACHABLE(); 0280 } 0281 recordJob(commandType, copyJob->srcUrls(), copyJob->destUrl(), copyJob); 0282 } 0283 0284 void FileUndoManagerPrivate::addCommand(const UndoCommand &cmd) 0285 { 0286 pushCommand(cmd); 0287 Q_EMIT q->jobRecordingFinished(cmd.m_type); 0288 } 0289 0290 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 79) 0291 bool FileUndoManager::undoAvailable() const 0292 { 0293 return isUndoAvailable(); 0294 } 0295 #endif 0296 0297 bool FileUndoManager::isUndoAvailable() const 0298 { 0299 return !d->m_commands.isEmpty() && !d->m_lock; 0300 } 0301 0302 QString FileUndoManager::undoText() const 0303 { 0304 if (d->m_commands.isEmpty()) { 0305 return i18n("Und&o"); 0306 } 0307 0308 FileUndoManager::CommandType t = d->m_commands.top().m_type; 0309 switch (t) { 0310 case FileUndoManager::Copy: 0311 return i18n("Und&o: Copy"); 0312 case FileUndoManager::Link: 0313 return i18n("Und&o: Link"); 0314 case FileUndoManager::Move: 0315 return i18n("Und&o: Move"); 0316 case FileUndoManager::Rename: 0317 return i18n("Und&o: Rename"); 0318 case FileUndoManager::Trash: 0319 return i18n("Und&o: Trash"); 0320 case FileUndoManager::Mkdir: 0321 return i18n("Und&o: Create Folder"); 0322 case FileUndoManager::Mkpath: 0323 return i18n("Und&o: Create Folder(s)"); 0324 case FileUndoManager::Put: 0325 return i18n("Und&o: Create File"); 0326 case FileUndoManager::BatchRename: 0327 return i18n("Und&o: Batch Rename"); 0328 } 0329 /* NOTREACHED */ 0330 return QString(); 0331 } 0332 0333 quint64 FileUndoManager::newCommandSerialNumber() 0334 { 0335 return ++(d->m_nextCommandIndex); 0336 } 0337 0338 quint64 FileUndoManager::currentCommandSerialNumber() const 0339 { 0340 if (!d->m_commands.isEmpty()) { 0341 const UndoCommand &cmd = d->m_commands.top(); 0342 Q_ASSERT(cmd.m_valid); 0343 return cmd.m_serialNumber; 0344 } 0345 0346 return 0; 0347 } 0348 0349 void FileUndoManager::undo() 0350 { 0351 Q_ASSERT(!d->m_commands.isEmpty()); // forgot to record before calling undo? 0352 0353 // Make a copy of the command to undo before slotPop() pops it. 0354 UndoCommand cmd = d->m_commands.last(); 0355 Q_ASSERT(cmd.m_valid); 0356 d->m_currentCmd = cmd; 0357 const CommandType commandType = cmd.m_type; 0358 0359 // Note that m_opQueue is empty for simple operations like Mkdir. 0360 const auto &opQueue = d->m_currentCmd.m_opQueue; 0361 0362 // Let's first ask for confirmation if we need to delete any file (#99898) 0363 QList<QUrl> itemsToDelete; 0364 for (auto it = opQueue.crbegin(); it != opQueue.crend(); ++it) { 0365 const BasicOperation &op = *it; 0366 const auto destination = op.m_dst; 0367 if (op.m_type == BasicOperation::File && commandType == FileUndoManager::Copy) { 0368 if (destination.isLocalFile() && !QFileInfo::exists(destination.toLocalFile())) { 0369 continue; 0370 } 0371 itemsToDelete.append(destination); 0372 } else if (commandType == FileUndoManager::Mkpath) { 0373 itemsToDelete.append(destination); 0374 } 0375 } 0376 if (commandType == FileUndoManager::Mkdir || commandType == FileUndoManager::Put) { 0377 itemsToDelete.append(d->m_currentCmd.m_dst); 0378 } 0379 if (!itemsToDelete.isEmpty()) { 0380 AskUserActionInterface *askUserInterface = nullptr; 0381 d->m_uiInterface->virtual_hook(UiInterface::HookGetAskUserActionInterface, &askUserInterface); 0382 if (askUserInterface) { 0383 if (!d->m_connectedToAskUserInterface) { 0384 d->m_connectedToAskUserInterface = true; 0385 QObject::connect(askUserInterface, &KIO::AskUserActionInterface::askUserDeleteResult, this, [=](bool allowDelete) { 0386 if (allowDelete) { 0387 d->startUndo(); 0388 } 0389 }); 0390 } 0391 0392 // Because undo can happen with an accidental Ctrl-Z, we want to always confirm. 0393 askUserInterface->askUserDelete(itemsToDelete, 0394 KIO::AskUserActionInterface::Delete, 0395 KIO::AskUserActionInterface::ForceConfirmation, 0396 d->m_uiInterface->parentWidget()); 0397 return; 0398 } 0399 } 0400 0401 d->startUndo(); 0402 } 0403 0404 void FileUndoManagerPrivate::startUndo() 0405 { 0406 slotPop(); 0407 slotLock(); 0408 0409 m_dirCleanupStack.clear(); 0410 m_dirStack.clear(); 0411 m_dirsToUpdate.clear(); 0412 0413 m_undoState = MOVINGFILES; 0414 0415 // Let's have a look at the basic operations we need to undo. 0416 auto &opQueue = m_currentCmd.m_opQueue; 0417 for (auto it = opQueue.rbegin(); it != opQueue.rend(); ++it) { 0418 const BasicOperation::Type type = (*it).m_type; 0419 if (type == BasicOperation::Directory && !(*it).m_renamed) { 0420 // If any directory has to be created/deleted, we'll start with that 0421 m_undoState = MAKINGDIRS; 0422 // Collect all the dirs that have to be created in case of a move undo. 0423 if (m_currentCmd.isMoveOrRename()) { 0424 m_dirStack.push((*it).m_src); 0425 } 0426 // Collect all dirs that have to be deleted 0427 // from the destination in both cases (copy and move). 0428 m_dirCleanupStack.prepend((*it).m_dst); 0429 } else if (type == BasicOperation::Link) { 0430 m_fileCleanupStack.prepend((*it).m_dst); 0431 } 0432 } 0433 auto isBasicOperation = [this](const BasicOperation &op) { 0434 return (op.m_type == BasicOperation::Directory && !op.m_renamed) // 0435 || (op.m_type == BasicOperation::Link && !m_currentCmd.isMoveOrRename()); 0436 }; 0437 opQueue.erase(std::remove_if(opQueue.begin(), opQueue.end(), isBasicOperation), opQueue.end()); 0438 0439 const FileUndoManager::CommandType commandType = m_currentCmd.m_type; 0440 if (commandType == FileUndoManager::Put) { 0441 m_fileCleanupStack.append(m_currentCmd.m_dst); 0442 } 0443 0444 qCDebug(KIO_WIDGETS) << "starting with" << undoStateToString(m_undoState); 0445 m_undoJob = new UndoJob(m_uiInterface->showProgressInfo()); 0446 auto undoFunc = [this]() { 0447 undoStep(); 0448 }; 0449 QMetaObject::invokeMethod(this, undoFunc, Qt::QueuedConnection); 0450 } 0451 0452 void FileUndoManagerPrivate::stopUndo(bool step) 0453 { 0454 m_currentCmd.m_opQueue.clear(); 0455 m_dirCleanupStack.clear(); 0456 m_fileCleanupStack.clear(); 0457 m_undoState = REMOVINGDIRS; 0458 m_undoJob = nullptr; 0459 0460 if (m_currentJob) { 0461 m_currentJob->kill(); 0462 } 0463 0464 m_currentJob = nullptr; 0465 0466 if (step) { 0467 undoStep(); 0468 } 0469 } 0470 0471 void FileUndoManagerPrivate::slotResult(KJob *job) 0472 { 0473 m_currentJob = nullptr; 0474 if (job->error()) { 0475 qWarning() << job->errorString(); 0476 m_uiInterface->jobError(static_cast<KIO::Job *>(job)); 0477 delete m_undoJob; 0478 stopUndo(false); 0479 } else if (m_undoState == STATINGFILE) { 0480 const BasicOperation op = m_currentCmd.m_opQueue.head(); 0481 // qDebug() << "stat result for " << op.m_dst; 0482 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job); 0483 const QDateTime mtime = QDateTime::fromSecsSinceEpoch(statJob->statResult().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), Qt::UTC); 0484 if (mtime != op.m_mtime) { 0485 qCDebug(KIO_WIDGETS) << op.m_dst << "was modified after being copied. Initial timestamp" << mtime << "now" << op.m_mtime; 0486 QDateTime srcTime = op.m_mtime.toLocalTime(); 0487 QDateTime destTime = mtime.toLocalTime(); 0488 if (!m_uiInterface->copiedFileWasModified(op.m_src, op.m_dst, srcTime, destTime)) { 0489 stopUndo(false); 0490 } 0491 } 0492 } 0493 0494 undoStep(); 0495 } 0496 0497 void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url) 0498 { 0499 if (!m_dirsToUpdate.contains(url)) { 0500 m_dirsToUpdate.prepend(url); 0501 } 0502 } 0503 0504 void FileUndoManagerPrivate::undoStep() 0505 { 0506 m_currentJob = nullptr; 0507 0508 if (m_undoState == MAKINGDIRS) { 0509 stepMakingDirectories(); 0510 } 0511 0512 if (m_undoState == MOVINGFILES || m_undoState == STATINGFILE) { 0513 stepMovingFiles(); 0514 } 0515 0516 if (m_undoState == REMOVINGLINKS) { 0517 stepRemovingLinks(); 0518 } 0519 0520 if (m_undoState == REMOVINGDIRS) { 0521 stepRemovingDirectories(); 0522 } 0523 0524 if (m_currentJob) { 0525 if (m_uiInterface) { 0526 KJobWidgets::setWindow(m_currentJob, m_uiInterface->parentWidget()); 0527 } 0528 QObject::connect(m_currentJob, &KJob::result, this, &FileUndoManagerPrivate::slotResult); 0529 } 0530 } 0531 0532 void FileUndoManagerPrivate::stepMakingDirectories() 0533 { 0534 if (!m_dirStack.isEmpty()) { 0535 QUrl dir = m_dirStack.pop(); 0536 // qDebug() << "creatingDir" << dir; 0537 m_currentJob = KIO::mkdir(dir); 0538 m_currentJob->setParentJob(m_undoJob); 0539 m_undoJob->emitCreatingDir(dir); 0540 } else { 0541 m_undoState = MOVINGFILES; 0542 } 0543 } 0544 0545 // Misnamed method: It moves files back, but it also 0546 // renames directories back, recreates symlinks, 0547 // deletes copied files, and restores trashed files. 0548 void FileUndoManagerPrivate::stepMovingFiles() 0549 { 0550 if (m_currentCmd.m_opQueue.isEmpty()) { 0551 m_undoState = REMOVINGLINKS; 0552 return; 0553 } 0554 0555 const BasicOperation op = m_currentCmd.m_opQueue.head(); 0556 Q_ASSERT(op.m_valid); 0557 if (op.m_type == BasicOperation::Directory || op.m_type == BasicOperation::Item) { 0558 Q_ASSERT(op.m_renamed); 0559 // qDebug() << "rename" << op.m_dst << op.m_src; 0560 m_currentJob = KIO::rename(op.m_dst, op.m_src, KIO::HideProgressInfo); 0561 m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type); 0562 } else if (op.m_type == BasicOperation::Link) { 0563 // qDebug() << "symlink" << op.m_target << op.m_src; 0564 m_currentJob = KIO::symlink(op.m_target, op.m_src, KIO::Overwrite | KIO::HideProgressInfo); 0565 } else if (m_currentCmd.m_type == FileUndoManager::Copy) { 0566 if (m_undoState == MOVINGFILES) { // dest not stat'ed yet 0567 // Before we delete op.m_dst, let's check if it was modified (#20532) 0568 // qDebug() << "stat" << op.m_dst; 0569 m_currentJob = KIO::stat(op.m_dst, KIO::HideProgressInfo); 0570 m_undoState = STATINGFILE; // temporarily 0571 return; // no pop() yet, we'll finish the work in slotResult 0572 } else { // dest was stat'ed, and the deletion was approved in slotResult 0573 m_currentJob = KIO::file_delete(op.m_dst, KIO::HideProgressInfo); 0574 m_undoJob->emitDeleting(op.m_dst); 0575 m_undoState = MOVINGFILES; 0576 } 0577 } else if (m_currentCmd.isMoveOrRename() || m_currentCmd.m_type == FileUndoManager::Trash) { 0578 m_currentJob = KIO::file_move(op.m_dst, op.m_src, -1, KIO::HideProgressInfo); 0579 m_currentJob->uiDelegateExtension()->createClipboardUpdater(m_currentJob, JobUiDelegateExtension::UpdateContent); 0580 m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type); 0581 } 0582 0583 if (m_currentJob) { 0584 m_currentJob->setParentJob(m_undoJob); 0585 } 0586 0587 m_currentCmd.m_opQueue.dequeue(); 0588 // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification 0589 // So we need to do it ourselves (but schedule it to the end of the undo, to compress them) 0590 QUrl url = op.m_dst.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0591 addDirToUpdate(url); 0592 0593 url = op.m_src.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0594 addDirToUpdate(url); 0595 } 0596 0597 void FileUndoManagerPrivate::stepRemovingLinks() 0598 { 0599 // qDebug() << "REMOVINGLINKS"; 0600 if (!m_fileCleanupStack.isEmpty()) { 0601 const QUrl file = m_fileCleanupStack.pop(); 0602 // qDebug() << "file_delete" << file; 0603 m_currentJob = KIO::file_delete(file, KIO::HideProgressInfo); 0604 m_currentJob->setParentJob(m_undoJob); 0605 m_undoJob->emitDeleting(file); 0606 0607 const QUrl url = file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0608 addDirToUpdate(url); 0609 } else { 0610 m_undoState = REMOVINGDIRS; 0611 0612 if (m_dirCleanupStack.isEmpty() && m_currentCmd.m_type == FileUndoManager::Mkdir) { 0613 m_dirCleanupStack << m_currentCmd.m_dst; 0614 } 0615 } 0616 } 0617 0618 void FileUndoManagerPrivate::stepRemovingDirectories() 0619 { 0620 if (!m_dirCleanupStack.isEmpty()) { 0621 QUrl dir = m_dirCleanupStack.pop(); 0622 // qDebug() << "rmdir" << dir; 0623 m_currentJob = KIO::rmdir(dir); 0624 m_currentJob->setParentJob(m_undoJob); 0625 m_undoJob->emitDeleting(dir); 0626 addDirToUpdate(dir); 0627 } else { 0628 m_currentCmd.m_valid = false; 0629 m_currentJob = nullptr; 0630 if (m_undoJob) { 0631 // qDebug() << "deleting undojob"; 0632 m_undoJob->emitResult(); 0633 m_undoJob = nullptr; 0634 } 0635 for (const QUrl &url : std::as_const(m_dirsToUpdate)) { 0636 // qDebug() << "Notifying FilesAdded for " << url; 0637 org::kde::KDirNotify::emitFilesAdded(url); 0638 } 0639 Q_EMIT q->undoJobFinished(); 0640 slotUnlock(); 0641 } 0642 } 0643 0644 // const ref doesn't work due to QDataStream 0645 void FileUndoManagerPrivate::slotPush(QByteArray data) 0646 { 0647 QDataStream strm(&data, QIODevice::ReadOnly); 0648 UndoCommand cmd; 0649 strm >> cmd; 0650 pushCommand(cmd); 0651 } 0652 0653 void FileUndoManagerPrivate::pushCommand(const UndoCommand &cmd) 0654 { 0655 m_commands.push(cmd); 0656 Q_EMIT q->undoAvailable(true); 0657 Q_EMIT q->undoTextChanged(q->undoText()); 0658 } 0659 0660 void FileUndoManagerPrivate::slotPop() 0661 { 0662 m_commands.pop(); 0663 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0664 Q_EMIT q->undoTextChanged(q->undoText()); 0665 } 0666 0667 void FileUndoManagerPrivate::slotLock() 0668 { 0669 // Q_ASSERT(!m_lock); 0670 m_lock = true; 0671 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0672 } 0673 0674 void FileUndoManagerPrivate::slotUnlock() 0675 { 0676 // Q_ASSERT(m_lock); 0677 m_lock = false; 0678 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0679 } 0680 0681 QByteArray FileUndoManagerPrivate::get() const 0682 { 0683 QByteArray data; 0684 QDataStream stream(&data, QIODevice::WriteOnly); 0685 stream << m_commands; 0686 return data; 0687 } 0688 0689 void FileUndoManager::setUiInterface(UiInterface *ui) 0690 { 0691 d->m_uiInterface.reset(ui); 0692 } 0693 0694 FileUndoManager::UiInterface *FileUndoManager::uiInterface() const 0695 { 0696 return d->m_uiInterface.get(); 0697 } 0698 0699 //// 0700 0701 class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate 0702 { 0703 public: 0704 QWidget *m_parentWidget = nullptr; 0705 bool m_showProgressInfo = true; 0706 }; 0707 0708 FileUndoManager::UiInterface::UiInterface() 0709 : d(new UiInterfacePrivate) 0710 { 0711 } 0712 0713 FileUndoManager::UiInterface::~UiInterface() = default; 0714 0715 void FileUndoManager::UiInterface::jobError(KIO::Job *job) 0716 { 0717 job->uiDelegate()->showErrorMessage(); 0718 } 0719 0720 bool FileUndoManager::UiInterface::copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) 0721 { 0722 Q_UNUSED(srcTime); // not sure it should appear in the msgbox 0723 // Possible improvement: only show the time if date is today 0724 const QString timeStr = QLocale().toString(destTime, QLocale::ShortFormat); 0725 const QString msg = i18n( 0726 "The file %1 was copied from %2, but since then it has apparently been modified at %3.\n" 0727 "Undoing the copy will delete the file, and all modifications will be lost.\n" 0728 "Are you sure you want to delete %4?", 0729 dest.toDisplayString(QUrl::PreferLocalFile), 0730 src.toDisplayString(QUrl::PreferLocalFile), 0731 timeStr, 0732 dest.toDisplayString(QUrl::PreferLocalFile)); 0733 0734 const auto result = KMessageBox::warningContinueCancel(d->m_parentWidget, 0735 msg, 0736 i18n("Undo File Copy Confirmation"), 0737 KStandardGuiItem::cont(), 0738 KStandardGuiItem::cancel(), 0739 QString(), 0740 KMessageBox::Options(KMessageBox::Notify) | KMessageBox::Dangerous); 0741 return result == KMessageBox::Continue; 0742 } 0743 0744 bool FileUndoManager::UiInterface::confirmDeletion(const QList<QUrl> &files) 0745 { 0746 KIO::JobUiDelegate uiDelegate(JobUiDelegate::Version::V2); 0747 uiDelegate.setWindow(d->m_parentWidget); 0748 // Because undo can happen with an accidental Ctrl-Z, we want to always confirm. 0749 return uiDelegate.askDeleteConfirmation(files, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::ForceConfirmation); 0750 } 0751 0752 QWidget *FileUndoManager::UiInterface::parentWidget() const 0753 { 0754 return d->m_parentWidget; 0755 } 0756 0757 void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget) 0758 { 0759 d->m_parentWidget = parentWidget; 0760 } 0761 0762 void FileUndoManager::UiInterface::setShowProgressInfo(bool b) 0763 { 0764 d->m_showProgressInfo = b; 0765 } 0766 0767 bool FileUndoManager::UiInterface::showProgressInfo() const 0768 { 0769 return d->m_showProgressInfo; 0770 } 0771 0772 void FileUndoManager::UiInterface::virtual_hook(int id, void *data) 0773 { 0774 if (id == HookGetAskUserActionInterface) { 0775 auto *p = static_cast<AskUserActionInterface **>(data); 0776 static KJobUiDelegate *delegate = KIO::createDefaultJobUiDelegate(); 0777 static auto *askUserInterface = delegate ? delegate->findChild<AskUserActionInterface *>(QString(), Qt::FindDirectChildrenOnly) : nullptr; 0778 *p = askUserInterface; 0779 } 0780 } 0781 0782 #include "fileundomanager.moc" 0783 #include "moc_fileundomanager.cpp" 0784 #include "moc_fileundomanager_p.cpp"