From 7f35195686e2efcaedfa8061787f930f960eb305 Mon Sep 17 00:00:00 2001 From: Naomi <103967@gmail.com> Date: Fri, 23 Aug 2024 09:41:31 +0200 Subject: [PATCH] Add file conflict dialog Signed-off-by: Naomi <103967@gmail.com> --- launcher/CMakeLists.txt | 3 + launcher/FileSystem.cpp | 1 + .../minecraft/UpdateGlobalDirectoriesTask.cpp | 71 +++++++++-- launcher/ui/dialogs/FileConflictDialog.cpp | 85 +++++++++++++ launcher/ui/dialogs/FileConflictDialog.h | 34 ++++++ launcher/ui/dialogs/FileConflictDialog.ui | 114 ++++++++++++++++++ 6 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 launcher/ui/dialogs/FileConflictDialog.cpp create mode 100644 launcher/ui/dialogs/FileConflictDialog.h create mode 100644 launcher/ui/dialogs/FileConflictDialog.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 45fc13de1..17280d37b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -999,6 +999,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ExportPackDialog.h ui/dialogs/ExportToModListDialog.cpp ui/dialogs/ExportToModListDialog.h + ui/dialogs/FileConflictDialog.cpp + ui/dialogs/FileConflictDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp @@ -1174,6 +1176,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportToModListDialog.ui + ui/dialogs/FileConflictDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 565b2e982..f3ec3a9ea 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1737,4 +1737,5 @@ QString getSymLinkTarget(const QString& path) { return QFileInfo(path).symLinkTarget(); } + } // namespace FS diff --git a/launcher/minecraft/UpdateGlobalDirectoriesTask.cpp b/launcher/minecraft/UpdateGlobalDirectoriesTask.cpp index 7c470074b..79927cd0a 100644 --- a/launcher/minecraft/UpdateGlobalDirectoriesTask.cpp +++ b/launcher/minecraft/UpdateGlobalDirectoriesTask.cpp @@ -1,15 +1,67 @@ #include "UpdateGlobalDirectoriesTask.h" +#include + #include "Application.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/FileConflictDialog.h" + +/** + * @brief Move a file or folder, and ask the user what to do in case of a conflict. + * @param source What to move. + * @param destination Where to move it to. + * @param recursive If true, all direct children will be moved 1 by 1. + * If false, the source will be directly moved to the destination. + * @param parent The parent of the dialog. + * @return True if everything could be moved. + */ +bool interactiveMove(const QString& source, const QString& destination, bool recursive = false, QWidget* parent = nullptr) +{ + const QFileInfo sourceInfo(source); + + // Make sure the source exists. + if (!sourceInfo.exists()) + return false; + + if (recursive) { + // Recursive doesn't make sense if the source isn't a directory. + if (!sourceInfo.isDir()) + return false; + + QDirIterator sourceIt(source, QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden | QDir::Filter::NoDotAndDotDot); + + while (sourceIt.hasNext()) { + if (!interactiveMove(sourceIt.next(), FS::PathCombine(destination, sourceIt.fileName()), false)) + return false; + } + + return true; + } + + if (QFile(destination).exists()) { + FileConflictDialog dialog(source, destination, true, parent); + FileConflictDialog::Result result = dialog.execWithResult(); + + if (result == FileConflictDialog::Cancel) + return false; + else if (result == FileConflictDialog::ChooseDestination) + return FS::deletePath(source); + } + + return FS::move(source, destination); +} class TryCreateSymlinkTask : public Task { public: - explicit TryCreateSymlinkTask(const QString& source, const QString& destination, MinecraftInstance* instance, const QString& setting) - : m_source(source), m_destination(destination), m_inst(instance), m_setting(setting) + explicit TryCreateSymlinkTask(const QString& source, + const QString& destination, + MinecraftInstance* instance, + const QString& setting, + QWidget* parent) + : m_source(source), m_destination(destination), m_inst(instance), m_setting(setting), m_parent(parent) { setObjectName("TryCreateSymlinkTask"); } @@ -49,8 +101,10 @@ class TryCreateSymlinkTask : public Task { FS::deletePath(m_destination); } else if (FS::checkFolderPathExists(m_destination)) { if (!FS::checkFolderPathEmpty(m_destination)) { - fail(tr("Failed to create global folder.\nEnsure that \"%1\" is empty.").arg(m_destination)); - return; + if (!interactiveMove(m_destination, m_source, true, m_parent)) { + fail(tr("Failed to create global folder.\nEnsure that \"%1\" is empty.").arg(m_destination)); + return; + } } FS::deletePath(m_destination); @@ -81,6 +135,7 @@ class TryCreateSymlinkTask : public Task { QString m_destination; MinecraftInstance* m_inst; QString m_setting; + QWidget* m_parent; }; UpdateGlobalDirectoriesTask::UpdateGlobalDirectoriesTask(MinecraftInstance* inst, QWidget* parent) @@ -94,22 +149,22 @@ void UpdateGlobalDirectoriesTask::executeTask() auto tasks = makeShared(this, "UpdateGlobalDirectoriesTask"); auto screenshotsTask = makeShared(m_inst->settings()->get("GlobalScreenshotsPath").toString(), - m_inst->screenshotsDir(), m_inst, "UseGlobalScreenshotsFolder"); + m_inst->screenshotsDir(), m_inst, "UseGlobalScreenshotsFolder", m_parent); connect(screenshotsTask.get(), &Task::failed, this, &UpdateGlobalDirectoriesTask::notifyFailed); tasks->addTask(screenshotsTask); auto savesTask = makeShared(m_inst->settings()->get("GlobalSavesPath").toString(), m_inst->worldDir(), m_inst, - "UseGlobalSavesFolder"); + "UseGlobalSavesFolder", m_parent); connect(savesTask.get(), &Task::failed, this, &UpdateGlobalDirectoriesTask::notifyFailed); tasks->addTask(savesTask); auto resoucePacksTask = makeShared(m_inst->settings()->get("GlobalResourcePacksPath").toString(), - m_inst->resourcePacksDir(), m_inst, "UseGlobalResourcePacksFolder"); + m_inst->resourcePacksDir(), m_inst, "UseGlobalResourcePacksFolder", m_parent); connect(resoucePacksTask.get(), &Task::failed, this, &UpdateGlobalDirectoriesTask::notifyFailed); tasks->addTask(resoucePacksTask); auto texturePacksTask = makeShared(m_inst->settings()->get("GlobalResourcePacksPath").toString(), - m_inst->texturePacksDir(), m_inst, "UseGlobalResourcePacksFolder"); + m_inst->texturePacksDir(), m_inst, "UseGlobalResourcePacksFolder", m_parent); connect(texturePacksTask.get(), &Task::failed, this, &UpdateGlobalDirectoriesTask::notifyFailed); tasks->addTask(texturePacksTask); diff --git a/launcher/ui/dialogs/FileConflictDialog.cpp b/launcher/ui/dialogs/FileConflictDialog.cpp new file mode 100644 index 000000000..891c17566 --- /dev/null +++ b/launcher/ui/dialogs/FileConflictDialog.cpp @@ -0,0 +1,85 @@ +#include "FileConflictDialog.h" +#include "ui_FileConflictDialog.h" + +#include +#include +#include + +#include "Application.h" + +FileConflictDialog::FileConflictDialog(QString source, QString destination, bool move, QWidget* parent) + : QDialog(parent), ui(new Ui::FileConflictDialog), m_result(Result::Cancel) +{ + ui->setupUi(this); + + QLocale locale; + + // Setup buttons + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &FileConflictDialog::cancel); + if (move) { + setWindowTitle("File conflict while moving files"); + + auto chooseSourceButton = ui->buttonBox->addButton(tr("Keep source"), QDialogButtonBox::DestructiveRole); + chooseSourceButton->setIcon(APPLICATION->getThemedIcon("delete")); + connect(chooseSourceButton, &QPushButton::clicked, this, &FileConflictDialog::chooseSource); + + auto chooseDestinationButton = ui->buttonBox->addButton(tr("Keep destination"), QDialogButtonBox::DestructiveRole); + chooseDestinationButton->setIcon(APPLICATION->getThemedIcon("delete")); + connect(chooseDestinationButton, &QPushButton::clicked, this, &FileConflictDialog::chooseDestination); + } else { + setWindowTitle("File conflict while copying files"); + + auto chooseSourceButton = ui->buttonBox->addButton(tr("Overwrite destination"), QDialogButtonBox::DestructiveRole); + chooseSourceButton->setIcon(APPLICATION->getThemedIcon("delete")); + connect(chooseSourceButton, &QPushButton::clicked, this, &FileConflictDialog::chooseSource); + + auto chooseDestinationButton = ui->buttonBox->addButton(tr("Skip"), QDialogButtonBox::DestructiveRole); + connect(chooseDestinationButton, &QPushButton::clicked, this, &FileConflictDialog::chooseDestination); + } + + // Setup info + QFileInfo sourceInfo(source); + ui->sourceInfoLabel->setText(tr("Name: %1
Size: %2
Last modified: %3") + .arg(source, sourceInfo.isDir() ? "-" : locale.formattedDataSize(sourceInfo.size()), + sourceInfo.lastModified().toString(locale.dateTimeFormat()))); + + QFileInfo destinationInfo(destination); + ui->destinationInfoLabel->setText(tr("Name: %1
Size: %2
Last modified: %3") + .arg(destination, + destinationInfo.isDir() ? "-" : locale.formattedDataSize(destinationInfo.size()), + destinationInfo.lastModified().toString(locale.dateTimeFormat()))); +} + +FileConflictDialog::~FileConflictDialog() +{ + delete ui; +} + +FileConflictDialog::Result FileConflictDialog::execWithResult() +{ + exec(); + return m_result; +} + +FileConflictDialog::Result FileConflictDialog::getResult() const +{ + return m_result; +} + +void FileConflictDialog::chooseSource() +{ + m_result = Result::ChooseSource; + accept(); +} + +void FileConflictDialog::chooseDestination() +{ + m_result = Result::ChooseDestination; + accept(); +} + +void FileConflictDialog::cancel() +{ + m_result = Result::Cancel; + reject(); +} diff --git a/launcher/ui/dialogs/FileConflictDialog.h b/launcher/ui/dialogs/FileConflictDialog.h new file mode 100644 index 000000000..02c55df93 --- /dev/null +++ b/launcher/ui/dialogs/FileConflictDialog.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace Ui { +class FileConflictDialog; +} + +class FileConflictDialog : public QDialog { + Q_OBJECT + + public: + enum Result { Cancel, ChooseSource, ChooseDestination }; + + /// @brief Create a new file conflict dialog + /// @param source The source path. What to copy/move. + /// @param destination The destination path. Where to copy/move. + /// @param move Whether the conflict is for a move or copy action + /// @param parent The parent of the dialog + explicit FileConflictDialog(QString source, QString destination, bool move = false, QWidget* parent = nullptr); + ~FileConflictDialog() override; + + Result execWithResult(); + Result getResult() const; + + private slots: + void chooseSource(); + void chooseDestination(); + void cancel(); + + private: + Ui::FileConflictDialog* ui; + Result m_result; +}; diff --git a/launcher/ui/dialogs/FileConflictDialog.ui b/launcher/ui/dialogs/FileConflictDialog.ui new file mode 100644 index 000000000..c71afd59a --- /dev/null +++ b/launcher/ui/dialogs/FileConflictDialog.ui @@ -0,0 +1,114 @@ + + + FileConflictDialog + + + + 0 + 0 + 411 + 219 + + + + + 0 + 0 + + + + + + + + + + Would you like to overwrite the destination? + + + Qt::AlignmentFlag::AlignCenter + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:700;">Source</span></p></body></html> + + + Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop + + + + + + + + 0 + 0 + + + + <html><head/><body><p><b>Name:</b></p><p>Size:</p><p>Date:</p></body></html> + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Destination</span></p></body></html> + + + Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop + + + + + + + <html><head/><body><p>Name:</p><p>Size:</p><p>Date:</p></body></html> + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel + + + + + + + +