diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7d4430fd2..af8da147c 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -321,6 +321,8 @@ set(MINECRAFT_SOURCES minecraft/World.cpp minecraft/WorldList.h minecraft/WorldList.cpp + minecraft/WorldTasks.h + minecraft/WorldTasks.cpp minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index ca8ac1aa8..1f5b5b417 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -34,6 +34,7 @@ */ #include "WorldList.h" +#include "WorldTasks.h" #include #include @@ -123,18 +124,23 @@ QString WorldList::instDirPath() const return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } -bool WorldList::deleteWorld(int index) +bool WorldList::removeWorldFromModel(const QFileInfo& sourceFile) { - if (index >= m_worlds.size() || index < 0) - return false; - World& m = m_worlds[index]; - if (m.destroy()) { - beginRemoveRows(QModelIndex(), index, index); - m_worlds.removeAt(index); + const auto sourcePath = sourceFile.absoluteFilePath(); + + for (int row = 0; row < m_worlds.size(); ++row) { + if (m_worlds.at(row).container().absoluteFilePath() != sourcePath) { + continue; + } + + beginRemoveRows(QModelIndex(), row, row); + m_worlds.removeAt(row); endRemoveRows(); + emit changed(); return true; } + return false; } @@ -360,6 +366,46 @@ void WorldList::installWorld(QFileInfo filename) w.install(m_dir.absolutePath()); } +std::unique_ptr WorldList::createInstallWorldTask(QFileInfo filename) +{ + return std::make_unique(InstallWorldTask::Args{ + .worlds = this, + .sourceFile = filename, + .targetDir = m_dir.absolutePath(), + }); +} + +std::unique_ptr WorldList::createCopyWorldTask(int index, const QString& name) +{ + if (index >= m_worlds.size() || index < 0) { + return nullptr; + } + + const auto& world = m_worlds.at(index); + + return std::make_unique(CopyWorldTask::Args{ + .worlds = this, + .sourceFile = world.container(), + .targetDir = m_dir.absolutePath(), + .targetName = name, + }); +} + +std::unique_ptr WorldList::createDeleteWorldTask(int index) +{ + if (index >= m_worlds.size() || index < 0) { + return nullptr; + } + + const auto& world = m_worlds.at(index); + + return std::make_unique(DeleteWorldTask::Args{ + .worlds = this, + .sourceFile = world.container(), + .displayName = world.name(), + }); +} + bool WorldList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 93fecf1f5..feb813fe9 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -20,10 +20,12 @@ #include #include #include +#include #include "BaseInstance.h" #include "minecraft/World.h" class QFileSystemWatcher; +class Task; class WorldList : public QAbstractListModel { Q_OBJECT @@ -50,8 +52,17 @@ class WorldList : public QAbstractListModel { /// Install a world from location void installWorld(QFileInfo filename); - /// Deletes the mod at the given index. - virtual bool deleteWorld(int index); + /// Create a task to install a world from location + std::unique_ptr createInstallWorldTask(QFileInfo filename); + + /// Create a task to copy the world at the given index. + std::unique_ptr createCopyWorldTask(int index, const QString& name); + + /// Create a task to delete the world at the given index. + std::unique_ptr createDeleteWorldTask(int index); + + /// Remove an already deleted world from the model. + bool removeWorldFromModel(const QFileInfo& sourceFile); /// Removes the world icon, if any virtual bool resetIcon(int index); diff --git a/launcher/minecraft/WorldTasks.cpp b/launcher/minecraft/WorldTasks.cpp new file mode 100644 index 000000000..0ca393d0f --- /dev/null +++ b/launcher/minecraft/WorldTasks.cpp @@ -0,0 +1,130 @@ +#include "WorldTasks.h" + +#include "World.h" +#include "WorldList.h" + +#include +#include +#include + +#include + +namespace { + +template +void invokeOnMainThread(Func&& func) +{ + auto app = QCoreApplication::instance(); + if (!app) { + return; + } + + QMetaObject::invokeMethod(app, std::forward(func), Qt::QueuedConnection); +} + +} // namespace + +InstallWorldTask::InstallWorldTask(Args args) : m_args(std::move(args)) {} + +void InstallWorldTask::executeTask() +{ + setStatus(tr("Importing world...")); + setDetails(m_args.sourceFile.fileName()); + setProgress(0, 0); + + QPointer self(this); + auto args = m_args; + + QThreadPool::globalInstance()->start([self, args]() mutable { + World world(args.sourceFile); + const bool ok = world.isValid() && world.install(args.targetDir); + + invokeOnMainThread([self, worlds = args.worlds, ok]() { + if (!self) { + return; + } + + if (!ok) { + self->emitFailed(self->tr("Failed to import world.")); + return; + } + + if (worlds) { + worlds->update(); + } + + self->setProgress(1, 1); + self->emitSucceeded(); + }); + }); +} + +CopyWorldTask::CopyWorldTask(Args args) : m_args(std::move(args)) {} + +void CopyWorldTask::executeTask() +{ + setStatus(tr("Copying world...")); + setDetails(m_args.targetName); + setProgress(0, 0); + + QPointer self(this); + auto args = m_args; + + QThreadPool::globalInstance()->start([self, args]() mutable { + World world(args.sourceFile); + const bool ok = world.isValid() && world.install(args.targetDir, args.targetName); + + invokeOnMainThread([self, worlds = args.worlds, ok]() { + if (!self) { + return; + } + + if (!ok) { + self->emitFailed(self->tr("Failed to copy world.")); + return; + } + + if (worlds) { + worlds->update(); + } + + self->setProgress(1, 1); + self->emitSucceeded(); + }); + }); +} + +DeleteWorldTask::DeleteWorldTask(Args args) : m_args(std::move(args)) {} + +void DeleteWorldTask::executeTask() +{ + setStatus(tr("Deleting world...")); + setDetails(m_args.displayName); + setProgress(0, 0); + + QPointer self(this); + auto args = m_args; + + QThreadPool::globalInstance()->start([self, args]() mutable { + World world(args.sourceFile); + const bool ok = world.destroy(); + + invokeOnMainThread([self, worlds = args.worlds, sourceFile = args.sourceFile, ok]() { + if (!self) { + return; + } + + if (!ok) { + self->emitFailed(self->tr("Failed to delete world.")); + return; + } + + if (worlds) { + worlds->removeWorldFromModel(sourceFile); + } + + self->setProgress(1, 1); + self->emitSucceeded(); + }); + }); +} \ No newline at end of file diff --git a/launcher/minecraft/WorldTasks.h b/launcher/minecraft/WorldTasks.h new file mode 100644 index 000000000..a4f49ebe8 --- /dev/null +++ b/launcher/minecraft/WorldTasks.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include + +#include "tasks/Task.h" + +class WorldList; + +class InstallWorldTask : public Task { + public: + struct Args { + QPointer worlds; + QFileInfo sourceFile; + QString targetDir; + }; + + explicit InstallWorldTask(Args args); + + protected: + void executeTask() override; + + private: + Args m_args; +}; + +class CopyWorldTask : public Task { + public: + struct Args { + QPointer worlds; + QFileInfo sourceFile; + QString targetDir; + QString targetName; + }; + + explicit CopyWorldTask(Args args); + + protected: + void executeTask() override; + + private: + Args m_args; +}; + +class DeleteWorldTask : public Task { + public: + struct Args { + QPointer worlds; + QFileInfo sourceFile; + QString displayName; + }; + + explicit DeleteWorldTask(Args args); + + protected: + void executeTask() override; + + private: + Args m_args; +}; \ No newline at end of file diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 71c370ab2..c70056a1b 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -38,6 +38,7 @@ #include "WorldListPage.h" #include "minecraft/WorldList.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" #include "ui_WorldListPage.h" #include @@ -187,23 +188,34 @@ bool WorldListPage::eventFilter(QObject* obj, QEvent* ev) void WorldListPage::on_actionRemove_triggered() { auto proxiedIndex = getSelectedWorld(); - - if (!proxiedIndex.isValid()) + if (!proxiedIndex.isValid()) { return; + } + + const auto& world = m_worlds->allWorlds().at(proxiedIndex.row()); auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("You are about to delete \"%1\".\n" "The world may be gone forever (A LONG TIME).\n\n" "Are you sure?") - .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()), + .arg(world.name()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (result != QMessageBox::Yes) { return; } + + auto task = m_worlds->createDeleteWorldTask(proxiedIndex.row()); + if (!task) { + return; + } + m_worlds->stopWatching(); - m_worlds->deleteWorld(proxiedIndex.row()); + + ProgressDialog dialog(this); + dialog.execWithTask(std::move(task)); + m_worlds->startWatching(); } @@ -393,13 +405,20 @@ void WorldListPage::on_actionAdd_triggered() { auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File") + " (*.zip)", QString(), this->parentWidget()); - if (!list.empty()) { - m_worlds->stopWatching(); - for (auto filename : list) { - m_worlds->installWorld(QFileInfo(filename)); - } - m_worlds->startWatching(); + if (list.empty()) { + return; } + + m_worlds->stopWatching(); + for (auto filename : list) { + auto task = m_worlds->createInstallWorldTask(QFileInfo(filename)); + if (!task) { + continue; + } + ProgressDialog dialog(this); + dialog.execWithTask(std::move(task)); + } + m_worlds->startWatching(); } bool WorldListPage::isWorldSafe(QModelIndex) @@ -426,18 +445,31 @@ void WorldListPage::on_actionCopy_triggered() return; } - if (!worldSafetyNagQuestion(tr("Copy World"))) + if (!worldSafetyNagQuestion(tr("Copy World"))) { return; + } + + const auto world = m_worlds->allWorlds().at(index.row()); - auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); - auto world = (World*)worldVariant.value(); bool ok = false; QString name = - QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok); + QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world.name(), &ok); - if (ok && name.length() > 0) { - world->install(m_worlds->dir().absolutePath(), name); + if (!ok || name.isEmpty()) { + return; } + + auto task = m_worlds->createCopyWorldTask(index.row(), name); + if (!task) { + return; + } + + m_worlds->stopWatching(); + + ProgressDialog dialog(this); + dialog.execWithTask(std::move(task)); + + m_worlds->startWatching(); } void WorldListPage::on_actionRename_triggered()