File indexing completed on 2025-02-09 04:24:24
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, QTimeZone::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 #if !defined(Q_OS_WIN) && !defined(Q_OS_MAC) 0240 (void)new KIOFileUndoManagerAdaptor(this); 0241 const QString dbusPath = QStringLiteral("/FileUndoManager"); 0242 const QString dbusInterface = QStringLiteral("org.kde.kio.FileUndoManager"); 0243 0244 QDBusConnection dbus = QDBusConnection::sessionBus(); 0245 dbus.registerObject(dbusPath, this); 0246 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("lock"), this, SLOT(slotLock())); 0247 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("pop"), this, SLOT(slotPop())); 0248 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("push"), this, SLOT(slotPush(QByteArray))); 0249 dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("unlock"), this, SLOT(slotUnlock())); 0250 #endif 0251 } 0252 0253 FileUndoManager::FileUndoManager() 0254 : d(new FileUndoManagerPrivate(this)) 0255 { 0256 } 0257 0258 FileUndoManager::~FileUndoManager() = default; 0259 0260 void FileUndoManager::recordJob(CommandType op, const QList<QUrl> &src, const QUrl &dst, KIO::Job *job) 0261 { 0262 // This records what the job does and calls addCommand when done 0263 (void)new CommandRecorder(op, src, dst, job); 0264 Q_EMIT jobRecordingStarted(op); 0265 } 0266 0267 void FileUndoManager::recordCopyJob(KIO::CopyJob *copyJob) 0268 { 0269 CommandType commandType; 0270 switch (copyJob->operationMode()) { 0271 case CopyJob::Copy: 0272 commandType = Copy; 0273 break; 0274 case CopyJob::Move: 0275 commandType = Move; 0276 break; 0277 case CopyJob::Link: 0278 commandType = Link; 0279 break; 0280 default: 0281 Q_UNREACHABLE(); 0282 } 0283 recordJob(commandType, copyJob->srcUrls(), copyJob->destUrl(), copyJob); 0284 } 0285 0286 void FileUndoManagerPrivate::addCommand(const UndoCommand &cmd) 0287 { 0288 pushCommand(cmd); 0289 Q_EMIT q->jobRecordingFinished(cmd.m_type); 0290 } 0291 0292 bool FileUndoManager::isUndoAvailable() const 0293 { 0294 return !d->m_commands.isEmpty() && !d->m_lock; 0295 } 0296 0297 QString FileUndoManager::undoText() const 0298 { 0299 if (d->m_commands.isEmpty()) { 0300 return i18n("Und&o"); 0301 } 0302 0303 FileUndoManager::CommandType t = d->m_commands.top().m_type; 0304 switch (t) { 0305 case FileUndoManager::Copy: 0306 return i18n("Und&o: Copy"); 0307 case FileUndoManager::Link: 0308 return i18n("Und&o: Link"); 0309 case FileUndoManager::Move: 0310 return i18n("Und&o: Move"); 0311 case FileUndoManager::Rename: 0312 return i18n("Und&o: Rename"); 0313 case FileUndoManager::Trash: 0314 return i18n("Und&o: Trash"); 0315 case FileUndoManager::Mkdir: 0316 return i18n("Und&o: Create Folder"); 0317 case FileUndoManager::Mkpath: 0318 return i18n("Und&o: Create Folder(s)"); 0319 case FileUndoManager::Put: 0320 return i18n("Und&o: Create File"); 0321 case FileUndoManager::BatchRename: 0322 return i18n("Und&o: Batch Rename"); 0323 } 0324 /* NOTREACHED */ 0325 return QString(); 0326 } 0327 0328 quint64 FileUndoManager::newCommandSerialNumber() 0329 { 0330 return ++(d->m_nextCommandIndex); 0331 } 0332 0333 quint64 FileUndoManager::currentCommandSerialNumber() const 0334 { 0335 if (!d->m_commands.isEmpty()) { 0336 const UndoCommand &cmd = d->m_commands.top(); 0337 Q_ASSERT(cmd.m_valid); 0338 return cmd.m_serialNumber; 0339 } 0340 0341 return 0; 0342 } 0343 0344 void FileUndoManager::undo() 0345 { 0346 Q_ASSERT(!d->m_commands.isEmpty()); // forgot to record before calling undo? 0347 0348 // Make a copy of the command to undo before slotPop() pops it. 0349 UndoCommand cmd = d->m_commands.last(); 0350 Q_ASSERT(cmd.m_valid); 0351 d->m_currentCmd = cmd; 0352 const CommandType commandType = cmd.m_type; 0353 0354 // Note that m_opQueue is empty for simple operations like Mkdir. 0355 const auto &opQueue = d->m_currentCmd.m_opQueue; 0356 0357 // Let's first ask for confirmation if we need to delete any file (#99898) 0358 QList<QUrl> itemsToDelete; 0359 for (auto it = opQueue.crbegin(); it != opQueue.crend(); ++it) { 0360 const BasicOperation &op = *it; 0361 const auto destination = op.m_dst; 0362 if (op.m_type == BasicOperation::File && commandType == FileUndoManager::Copy) { 0363 if (destination.isLocalFile() && !QFileInfo::exists(destination.toLocalFile())) { 0364 continue; 0365 } 0366 itemsToDelete.append(destination); 0367 } else if (commandType == FileUndoManager::Mkpath) { 0368 itemsToDelete.append(destination); 0369 } 0370 } 0371 if (commandType == FileUndoManager::Mkdir || commandType == FileUndoManager::Put) { 0372 itemsToDelete.append(d->m_currentCmd.m_dst); 0373 } 0374 if (!itemsToDelete.isEmpty()) { 0375 AskUserActionInterface *askUserInterface = nullptr; 0376 d->m_uiInterface->virtual_hook(UiInterface::HookGetAskUserActionInterface, &askUserInterface); 0377 if (askUserInterface) { 0378 if (!d->m_connectedToAskUserInterface) { 0379 d->m_connectedToAskUserInterface = true; 0380 QObject::connect(askUserInterface, &KIO::AskUserActionInterface::askUserDeleteResult, this, [=](bool allowDelete) { 0381 if (allowDelete) { 0382 d->startUndo(); 0383 } 0384 }); 0385 } 0386 0387 // Because undo can happen with an accidental Ctrl-Z, we want to always confirm. 0388 askUserInterface->askUserDelete(itemsToDelete, 0389 KIO::AskUserActionInterface::Delete, 0390 KIO::AskUserActionInterface::ForceConfirmation, 0391 d->m_uiInterface->parentWidget()); 0392 return; 0393 } 0394 } 0395 0396 d->startUndo(); 0397 } 0398 0399 void FileUndoManagerPrivate::startUndo() 0400 { 0401 slotPop(); 0402 slotLock(); 0403 0404 m_dirCleanupStack.clear(); 0405 m_dirStack.clear(); 0406 m_dirsToUpdate.clear(); 0407 0408 m_undoState = MOVINGFILES; 0409 0410 // Let's have a look at the basic operations we need to undo. 0411 auto &opQueue = m_currentCmd.m_opQueue; 0412 for (auto it = opQueue.rbegin(); it != opQueue.rend(); ++it) { 0413 const BasicOperation::Type type = (*it).m_type; 0414 if (type == BasicOperation::Directory && !(*it).m_renamed) { 0415 // If any directory has to be created/deleted, we'll start with that 0416 m_undoState = MAKINGDIRS; 0417 // Collect all the dirs that have to be created in case of a move undo. 0418 if (m_currentCmd.isMoveOrRename()) { 0419 m_dirStack.push((*it).m_src); 0420 } 0421 // Collect all dirs that have to be deleted 0422 // from the destination in both cases (copy and move). 0423 m_dirCleanupStack.prepend((*it).m_dst); 0424 } else if (type == BasicOperation::Link) { 0425 m_fileCleanupStack.prepend((*it).m_dst); 0426 } 0427 } 0428 auto isBasicOperation = [this](const BasicOperation &op) { 0429 return (op.m_type == BasicOperation::Directory && !op.m_renamed) // 0430 || (op.m_type == BasicOperation::Link && !m_currentCmd.isMoveOrRename()); 0431 }; 0432 opQueue.erase(std::remove_if(opQueue.begin(), opQueue.end(), isBasicOperation), opQueue.end()); 0433 0434 const FileUndoManager::CommandType commandType = m_currentCmd.m_type; 0435 if (commandType == FileUndoManager::Put) { 0436 m_fileCleanupStack.append(m_currentCmd.m_dst); 0437 } 0438 0439 qCDebug(KIO_WIDGETS) << "starting with" << undoStateToString(m_undoState); 0440 m_undoJob = new UndoJob(m_uiInterface->showProgressInfo()); 0441 auto undoFunc = [this]() { 0442 undoStep(); 0443 }; 0444 QMetaObject::invokeMethod(this, undoFunc, Qt::QueuedConnection); 0445 } 0446 0447 void FileUndoManagerPrivate::stopUndo(bool step) 0448 { 0449 m_currentCmd.m_opQueue.clear(); 0450 m_dirCleanupStack.clear(); 0451 m_fileCleanupStack.clear(); 0452 m_undoState = REMOVINGDIRS; 0453 m_undoJob = nullptr; 0454 0455 if (m_currentJob) { 0456 m_currentJob->kill(); 0457 } 0458 0459 m_currentJob = nullptr; 0460 0461 if (step) { 0462 undoStep(); 0463 } 0464 } 0465 0466 void FileUndoManagerPrivate::slotResult(KJob *job) 0467 { 0468 m_currentJob = nullptr; 0469 if (job->error()) { 0470 qWarning() << job->errorString(); 0471 m_uiInterface->jobError(static_cast<KIO::Job *>(job)); 0472 delete m_undoJob; 0473 stopUndo(false); 0474 } else if (m_undoState == STATINGFILE) { 0475 const BasicOperation op = m_currentCmd.m_opQueue.head(); 0476 // qDebug() << "stat result for " << op.m_dst; 0477 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job); 0478 const QDateTime mtime = QDateTime::fromSecsSinceEpoch(statJob->statResult().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), QTimeZone::UTC); 0479 if (mtime != op.m_mtime) { 0480 qCDebug(KIO_WIDGETS) << op.m_dst << "was modified after being copied. Initial timestamp" << mtime << "now" << op.m_mtime; 0481 QDateTime srcTime = op.m_mtime.toLocalTime(); 0482 QDateTime destTime = mtime.toLocalTime(); 0483 if (!m_uiInterface->copiedFileWasModified(op.m_src, op.m_dst, srcTime, destTime)) { 0484 stopUndo(false); 0485 } 0486 } 0487 } 0488 0489 undoStep(); 0490 } 0491 0492 void FileUndoManagerPrivate::addDirToUpdate(const QUrl &url) 0493 { 0494 if (!m_dirsToUpdate.contains(url)) { 0495 m_dirsToUpdate.prepend(url); 0496 } 0497 } 0498 0499 void FileUndoManagerPrivate::undoStep() 0500 { 0501 m_currentJob = nullptr; 0502 0503 if (m_undoState == MAKINGDIRS) { 0504 stepMakingDirectories(); 0505 } 0506 0507 if (m_undoState == MOVINGFILES || m_undoState == STATINGFILE) { 0508 stepMovingFiles(); 0509 } 0510 0511 if (m_undoState == REMOVINGLINKS) { 0512 stepRemovingLinks(); 0513 } 0514 0515 if (m_undoState == REMOVINGDIRS) { 0516 stepRemovingDirectories(); 0517 } 0518 0519 if (m_currentJob) { 0520 if (m_uiInterface) { 0521 KJobWidgets::setWindow(m_currentJob, m_uiInterface->parentWidget()); 0522 } 0523 QObject::connect(m_currentJob, &KJob::result, this, &FileUndoManagerPrivate::slotResult); 0524 } 0525 } 0526 0527 void FileUndoManagerPrivate::stepMakingDirectories() 0528 { 0529 if (!m_dirStack.isEmpty()) { 0530 QUrl dir = m_dirStack.pop(); 0531 // qDebug() << "creatingDir" << dir; 0532 m_currentJob = KIO::mkdir(dir); 0533 m_currentJob->setParentJob(m_undoJob); 0534 m_undoJob->emitCreatingDir(dir); 0535 } else { 0536 m_undoState = MOVINGFILES; 0537 } 0538 } 0539 0540 // Misnamed method: It moves files back, but it also 0541 // renames directories back, recreates symlinks, 0542 // deletes copied files, and restores trashed files. 0543 void FileUndoManagerPrivate::stepMovingFiles() 0544 { 0545 if (m_currentCmd.m_opQueue.isEmpty()) { 0546 m_undoState = REMOVINGLINKS; 0547 return; 0548 } 0549 0550 const BasicOperation op = m_currentCmd.m_opQueue.head(); 0551 Q_ASSERT(op.m_valid); 0552 if (op.m_type == BasicOperation::Directory || op.m_type == BasicOperation::Item) { 0553 Q_ASSERT(op.m_renamed); 0554 // qDebug() << "rename" << op.m_dst << op.m_src; 0555 m_currentJob = KIO::rename(op.m_dst, op.m_src, KIO::HideProgressInfo); 0556 m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type); 0557 } else if (op.m_type == BasicOperation::Link) { 0558 // qDebug() << "symlink" << op.m_target << op.m_src; 0559 m_currentJob = KIO::symlink(op.m_target, op.m_src, KIO::Overwrite | KIO::HideProgressInfo); 0560 } else if (m_currentCmd.m_type == FileUndoManager::Copy) { 0561 if (m_undoState == MOVINGFILES) { // dest not stat'ed yet 0562 // Before we delete op.m_dst, let's check if it was modified (#20532) 0563 // qDebug() << "stat" << op.m_dst; 0564 m_currentJob = KIO::stat(op.m_dst, KIO::HideProgressInfo); 0565 m_undoState = STATINGFILE; // temporarily 0566 return; // no pop() yet, we'll finish the work in slotResult 0567 } else { // dest was stat'ed, and the deletion was approved in slotResult 0568 m_currentJob = KIO::file_delete(op.m_dst, KIO::HideProgressInfo); 0569 m_undoJob->emitDeleting(op.m_dst); 0570 m_undoState = MOVINGFILES; 0571 } 0572 } else if (m_currentCmd.isMoveOrRename() || m_currentCmd.m_type == FileUndoManager::Trash) { 0573 m_currentJob = KIO::file_move(op.m_dst, op.m_src, -1, KIO::HideProgressInfo); 0574 m_currentJob->uiDelegateExtension()->createClipboardUpdater(m_currentJob, JobUiDelegateExtension::UpdateContent); 0575 m_undoJob->emitMovingOrRenaming(op.m_dst, op.m_src, m_currentCmd.m_type); 0576 } 0577 0578 if (m_currentJob) { 0579 m_currentJob->setParentJob(m_undoJob); 0580 } 0581 0582 m_currentCmd.m_opQueue.dequeue(); 0583 // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification 0584 // So we need to do it ourselves (but schedule it to the end of the undo, to compress them) 0585 QUrl url = op.m_dst.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0586 addDirToUpdate(url); 0587 0588 url = op.m_src.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0589 addDirToUpdate(url); 0590 } 0591 0592 void FileUndoManagerPrivate::stepRemovingLinks() 0593 { 0594 // qDebug() << "REMOVINGLINKS"; 0595 if (!m_fileCleanupStack.isEmpty()) { 0596 const QUrl file = m_fileCleanupStack.pop(); 0597 // qDebug() << "file_delete" << file; 0598 m_currentJob = KIO::file_delete(file, KIO::HideProgressInfo); 0599 m_currentJob->setParentJob(m_undoJob); 0600 m_undoJob->emitDeleting(file); 0601 0602 const QUrl url = file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0603 addDirToUpdate(url); 0604 } else { 0605 m_undoState = REMOVINGDIRS; 0606 0607 if (m_dirCleanupStack.isEmpty() && m_currentCmd.m_type == FileUndoManager::Mkdir) { 0608 m_dirCleanupStack << m_currentCmd.m_dst; 0609 } 0610 } 0611 } 0612 0613 void FileUndoManagerPrivate::stepRemovingDirectories() 0614 { 0615 if (!m_dirCleanupStack.isEmpty()) { 0616 QUrl dir = m_dirCleanupStack.pop(); 0617 // qDebug() << "rmdir" << dir; 0618 m_currentJob = KIO::rmdir(dir); 0619 m_currentJob->setParentJob(m_undoJob); 0620 m_undoJob->emitDeleting(dir); 0621 addDirToUpdate(dir); 0622 } else { 0623 m_currentCmd.m_valid = false; 0624 m_currentJob = nullptr; 0625 if (m_undoJob) { 0626 // qDebug() << "deleting undojob"; 0627 m_undoJob->emitResult(); 0628 m_undoJob = nullptr; 0629 } 0630 for (const QUrl &url : std::as_const(m_dirsToUpdate)) { 0631 // qDebug() << "Notifying FilesAdded for " << url; 0632 org::kde::KDirNotify::emitFilesAdded(url); 0633 } 0634 Q_EMIT q->undoJobFinished(); 0635 slotUnlock(); 0636 } 0637 } 0638 0639 // const ref doesn't work due to QDataStream 0640 void FileUndoManagerPrivate::slotPush(QByteArray data) 0641 { 0642 QDataStream strm(&data, QIODevice::ReadOnly); 0643 UndoCommand cmd; 0644 strm >> cmd; 0645 pushCommand(cmd); 0646 } 0647 0648 void FileUndoManagerPrivate::pushCommand(const UndoCommand &cmd) 0649 { 0650 m_commands.push(cmd); 0651 Q_EMIT q->undoAvailable(true); 0652 Q_EMIT q->undoTextChanged(q->undoText()); 0653 } 0654 0655 void FileUndoManagerPrivate::slotPop() 0656 { 0657 m_commands.pop(); 0658 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0659 Q_EMIT q->undoTextChanged(q->undoText()); 0660 } 0661 0662 void FileUndoManagerPrivate::slotLock() 0663 { 0664 // Q_ASSERT(!m_lock); 0665 m_lock = true; 0666 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0667 } 0668 0669 void FileUndoManagerPrivate::slotUnlock() 0670 { 0671 // Q_ASSERT(m_lock); 0672 m_lock = false; 0673 Q_EMIT q->undoAvailable(q->isUndoAvailable()); 0674 } 0675 0676 QByteArray FileUndoManagerPrivate::get() const 0677 { 0678 QByteArray data; 0679 QDataStream stream(&data, QIODevice::WriteOnly); 0680 stream << m_commands; 0681 return data; 0682 } 0683 0684 void FileUndoManager::setUiInterface(UiInterface *ui) 0685 { 0686 d->m_uiInterface.reset(ui); 0687 } 0688 0689 FileUndoManager::UiInterface *FileUndoManager::uiInterface() const 0690 { 0691 return d->m_uiInterface.get(); 0692 } 0693 0694 //// 0695 0696 class Q_DECL_HIDDEN FileUndoManager::UiInterface::UiInterfacePrivate 0697 { 0698 public: 0699 QPointer<QWidget> m_parentWidget; 0700 bool m_showProgressInfo = true; 0701 }; 0702 0703 FileUndoManager::UiInterface::UiInterface() 0704 : d(new UiInterfacePrivate) 0705 { 0706 } 0707 0708 FileUndoManager::UiInterface::~UiInterface() = default; 0709 0710 void FileUndoManager::UiInterface::jobError(KIO::Job *job) 0711 { 0712 job->uiDelegate()->showErrorMessage(); 0713 } 0714 0715 bool FileUndoManager::UiInterface::copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) 0716 { 0717 Q_UNUSED(srcTime); // not sure it should appear in the msgbox 0718 // Possible improvement: only show the time if date is today 0719 const QString timeStr = QLocale().toString(destTime, QLocale::ShortFormat); 0720 const QString msg = i18n( 0721 "The file %1 was copied from %2, but since then it has apparently been modified at %3.\n" 0722 "Undoing the copy will delete the file, and all modifications will be lost.\n" 0723 "Are you sure you want to delete %4?", 0724 dest.toDisplayString(QUrl::PreferLocalFile), 0725 src.toDisplayString(QUrl::PreferLocalFile), 0726 timeStr, 0727 dest.toDisplayString(QUrl::PreferLocalFile)); 0728 0729 const auto result = KMessageBox::warningContinueCancel(d->m_parentWidget, 0730 msg, 0731 i18n("Undo File Copy Confirmation"), 0732 KStandardGuiItem::cont(), 0733 KStandardGuiItem::cancel(), 0734 QString(), 0735 KMessageBox::Options(KMessageBox::Notify) | KMessageBox::Dangerous); 0736 return result == KMessageBox::Continue; 0737 } 0738 0739 QWidget *FileUndoManager::UiInterface::parentWidget() const 0740 { 0741 return d->m_parentWidget; 0742 } 0743 0744 void FileUndoManager::UiInterface::setParentWidget(QWidget *parentWidget) 0745 { 0746 d->m_parentWidget = parentWidget; 0747 } 0748 0749 void FileUndoManager::UiInterface::setShowProgressInfo(bool b) 0750 { 0751 d->m_showProgressInfo = b; 0752 } 0753 0754 bool FileUndoManager::UiInterface::showProgressInfo() const 0755 { 0756 return d->m_showProgressInfo; 0757 } 0758 0759 void FileUndoManager::UiInterface::virtual_hook(int id, void *data) 0760 { 0761 if (id == HookGetAskUserActionInterface) { 0762 auto *p = static_cast<AskUserActionInterface **>(data); 0763 static KJobUiDelegate *delegate = KIO::createDefaultJobUiDelegate(); 0764 static auto *askUserInterface = delegate ? delegate->findChild<AskUserActionInterface *>(QString(), Qt::FindDirectChildrenOnly) : nullptr; 0765 *p = askUserInterface; 0766 } 0767 } 0768 0769 #include "fileundomanager.moc" 0770 #include "moc_fileundomanager.cpp" 0771 #include "moc_fileundomanager_p.cpp"