From 2d63c860227f4a539526a85d8999f867ae67ce43 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:26:53 -0300 Subject: feat: make Task a QRunnable This makes it possible to run a task in another thread. I added a variable to toggle debug prints because they seem to trigger an assertion on Qt internals when the task in on another thread. Of course, this isn't awesome, but can wait until we improve our logging. Signed-off-by: flow --- launcher/tasks/Task.cpp | 24 ++++++++++++++++-------- launcher/tasks/Task.h | 12 ++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index bb71b98c..b4babdd4 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -37,8 +37,9 @@ #include -Task::Task(QObject *parent) : QObject(parent) +Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug) { + setAutoDelete(false); } void Task::setStatus(const QString &new_status) @@ -63,27 +64,32 @@ void Task::start() { case State::Inactive: { - qDebug() << "Task" << describe() << "starting for the first time"; + if (m_show_debug) + qDebug() << "Task" << describe() << "starting for the first time"; break; } case State::AbortedByUser: { - qDebug() << "Task" << describe() << "restarting for after being aborted by user"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after being aborted by user"; break; } case State::Failed: { - qDebug() << "Task" << describe() << "restarting for after failing at first"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after failing at first"; break; } case State::Succeeded: { - qDebug() << "Task" << describe() << "restarting for after succeeding at first"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after succeeding at first"; break; } case State::Running: { - qWarning() << "The launcher tried to start task" << describe() << "while it was already running!"; + if (m_show_debug) + qWarning() << "The launcher tried to start task" << describe() << "while it was already running!"; return; } } @@ -118,7 +124,8 @@ void Task::emitAborted() } m_state = State::AbortedByUser; m_failReason = "Aborted."; - qDebug() << "Task" << describe() << "aborted."; + if (m_show_debug) + qDebug() << "Task" << describe() << "aborted."; emit aborted(); emit finished(); } @@ -132,7 +139,8 @@ void Task::emitSucceeded() return; } m_state = State::Succeeded; - qDebug() << "Task" << describe() << "succeeded"; + if (m_show_debug) + qDebug() << "Task" << describe() << "succeeded"; emit succeeded(); emit finished(); } diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index aafaf68c..2baf0188 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -35,9 +35,11 @@ #pragma once +#include + #include "QObjectPtr.h" -class Task : public QObject { +class Task : public QObject, public QRunnable { Q_OBJECT public: using Ptr = shared_qobject_ptr; @@ -45,7 +47,7 @@ class Task : public QObject { enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: - explicit Task(QObject* parent = 0); + explicit Task(QObject* parent = 0, bool show_debug_log = true); virtual ~Task() = default; bool isRunning() const; @@ -95,6 +97,9 @@ class Task : public QObject { void stepStatus(QString status); public slots: + // QRunnable's interface + void run() override { start(); } + virtual void start(); virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; @@ -117,4 +122,7 @@ class Task : public QObject { QString m_status; int m_progress = 0; int m_progressTotal = 100; + + // TODO: Nuke in favor of QLoggingCategory + bool m_show_debug = true; }; -- cgit From 3225f514f64533394e14bf7aee4e61c19a72ed2f Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:53:50 -0300 Subject: refactor: move general info from Mod to Resource This allows us to create other resources that are not Mods, but can still share a significant portion of code. Signed-off-by: flow --- launcher/CMakeLists.txt | 2 + launcher/MMCZip.cpp | 6 +-- launcher/minecraft/MinecraftInstance.cpp | 2 +- launcher/minecraft/mod/Mod.cpp | 74 +++++------------------------ launcher/minecraft/mod/Mod.h | 45 ++---------------- launcher/minecraft/mod/Resource.cpp | 53 +++++++++++++++++++++ launcher/minecraft/mod/Resource.h | 74 +++++++++++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.cpp | 2 +- launcher/ui/widgets/MCModInfoFrame.cpp | 2 +- 9 files changed, 150 insertions(+), 110 deletions(-) create mode 100644 launcher/minecraft/mod/Resource.cpp create mode 100644 launcher/minecraft/mod/Resource.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cff07b4b..70997e51 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -318,6 +318,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp + minecraft/mod/Resource.h + minecraft/mod/Resource.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 04ca5094..9f4e968f 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // do not merge disabled mods. if (!mod->enabled()) continue; - if (mod->type() == Mod::MOD_ZIPFILE) + if (mod->type() == ResourceType::ZIPFILE) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { @@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const return false; } } - else if (mod->type() == Mod::MOD_SINGLEFILE) + else if (mod->type() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); @@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } addedFiles.insert(filename.fileName()); } - else if (mod->type() == Mod::MOD_FOLDER) + else if (mod->type() == ResourceType::FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index c677b677..b42aeda3 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -714,7 +714,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr }); for(auto mod: modList) { - if(mod->type() == Mod::MOD_FOLDER) + if(mod->type() == ResourceType::FOLDER) { out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 588d76e3..f28fd32a 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -36,13 +36,10 @@ #include "Mod.h" +#include #include #include -#include -#include - -#include "Application.h" #include "MetadataHandler.h" namespace { @@ -51,75 +48,27 @@ ModDetails invalidDetails; } -Mod::Mod(const QFileInfo& file) +Mod::Mod(const QFileInfo& file) : Resource(file) { - repath(file); - m_changedDateTime = file.lastModified(); + m_enabled = (file.suffix() != "disabled"); } Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) - : m_file(mods_dir.absoluteFilePath(metadata.filename)) - , m_internal_id(metadata.filename) - , m_name(metadata.name) + : Mod(mods_dir.absoluteFilePath(metadata.filename)) { - if (m_file.isDir()) { - m_type = MOD_FOLDER; - } else { - if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) - m_type = MOD_ZIPFILE; - else if (metadata.filename.endsWith(".litemod")) - m_type = MOD_LITEMOD; - else - m_type = MOD_SINGLEFILE; - } - - m_enabled = true; - m_changedDateTime = m_file.lastModified(); - + m_name = metadata.name; m_temp_metadata = std::make_shared(std::move(metadata)); } -void Mod::repath(const QFileInfo& file) -{ - m_file = file; - QString name_base = file.fileName(); - - m_type = Mod::MOD_UNKNOWN; - - m_internal_id = name_base; - - if (m_file.isDir()) { - m_type = MOD_FOLDER; - m_name = name_base; - } else if (m_file.isFile()) { - if (name_base.endsWith(".disabled")) { - m_enabled = false; - name_base.chop(9); - } else { - m_enabled = true; - } - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { - m_type = MOD_ZIPFILE; - name_base.chop(4); - } else if (name_base.endsWith(".litemod")) { - m_type = MOD_LITEMOD; - name_base.chop(8); - } else { - m_type = MOD_SINGLEFILE; - } - m_name = name_base; - } -} - auto Mod::enable(bool value) -> bool { - if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) return false; if (m_enabled == value) return false; - QString path = m_file.absoluteFilePath(); + QString path = m_file_info.absoluteFilePath(); QFile file(path); if (value) { if (!path.endsWith(".disabled")) @@ -136,7 +85,7 @@ auto Mod::enable(bool value) -> bool } if (status() == ModStatus::NoMetadata) - repath(QFileInfo(path)); + setFile(QFileInfo(path)); m_enabled = value; return true; @@ -175,8 +124,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool } } - m_type = MOD_UNKNOWN; - return FS::deletePath(m_file.filePath()); + return Resource::destroy(); } auto Mod::details() const -> const ModDetails& @@ -239,8 +187,8 @@ auto Mod::metadata() const -> const std::shared_ptr void Mod::finishResolvingWithDetails(std::shared_ptr details) { - m_resolving = false; - m_resolved = true; + m_is_resolving = false; + m_is_resolved = true; m_localDetails = details; setStatus(m_temp_status); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 7a13e44b..313c478c 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,38 +39,23 @@ #include #include -#include "QObjectPtr.h" +#include "Resource.h" #include "ModDetails.h" -class Mod : public QObject +class Mod : public Resource { Q_OBJECT public: - enum ModType - { - MOD_UNKNOWN, //!< Indicates an unspecified mod type. - MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. - MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). - MOD_FOLDER, //!< The mod is in a folder on the filesystem. - MOD_LITEMOD, //!< The mod is a litemod - }; - using Ptr = shared_qobject_ptr; Mod() = default; Mod(const QFileInfo &file); explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - auto fileinfo() const -> QFileInfo { return m_file; } - auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } - auto internal_id() const -> QString { return m_internal_id; } - auto type() const -> ModType { return m_type; } auto enabled() const -> bool { return m_enabled; } - auto valid() const -> bool { return m_type != MOD_UNKNOWN; } - auto details() const -> const ModDetails&; - auto name() const -> QString; + auto name() const -> QString override; auto version() const -> QString; auto homeurl() const -> QString; auto description() const -> QString; @@ -85,31 +70,12 @@ public: auto enable(bool value) -> bool; - // delete all the files of this mod + // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; - // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) - void repath(const QFileInfo &file); - - auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; } - auto isResolving() const -> bool { return m_resolving; } - auto resolutionTicket() const -> int { return m_resolutionTicket; } - - void setResolving(bool resolving, int resolutionTicket) { - m_resolving = resolving; - m_resolutionTicket = resolutionTicket; - } void finishResolvingWithDetails(std::shared_ptr details); protected: - QFileInfo m_file; - QDateTime m_changedDateTime; - - QString m_internal_id; - /* Name as reported via the file name */ - QString m_name; - ModType m_type = MOD_UNKNOWN; - /* If the mod has metadata, this will be filled in the constructor, and passed to * the ModDetails when calling finishResolvingWithDetails */ std::shared_ptr m_temp_metadata; @@ -120,7 +86,4 @@ protected: std::shared_ptr m_localDetails; bool m_enabled = true; - bool m_resolving = false; - bool m_resolved = false; - int m_resolutionTicket = 0; }; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp new file mode 100644 index 00000000..8771a20f --- /dev/null +++ b/launcher/minecraft/mod/Resource.cpp @@ -0,0 +1,53 @@ +#include "Resource.h" + +#include "FileSystem.h" + +Resource::Resource(QObject* parent) : QObject(parent) {} + +Resource::Resource(QFileInfo file_info) : QObject() +{ + setFile(file_info); +} + +void Resource::setFile(QFileInfo file_info) +{ + m_file_info = file_info; + parseFile(); +} + +void Resource::parseFile() +{ + QString file_name{ m_file_info.fileName() }; + + m_type = ResourceType::UNKNOWN; + + m_internal_id = file_name; + + if (m_file_info.isDir()) { + m_type = ResourceType::FOLDER; + m_name = file_name; + } else if (m_file_info.isFile()) { + if (file_name.endsWith(".disabled")) + file_name.chop(9); + + if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { + m_type = ResourceType::ZIPFILE; + file_name.chop(4); + } else if (file_name.endsWith(".litemod")) { + m_type = ResourceType::LITEMOD; + file_name.chop(8); + } else { + m_type = ResourceType::SINGLEFILE; + } + + m_name = file_name; + } + + m_changed_date_time = m_file_info.lastModified(); +} + +bool Resource::destroy() +{ + m_type = ResourceType::UNKNOWN; + return FS::deletePath(m_file_info.filePath()); +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h new file mode 100644 index 00000000..c348c7e2 --- /dev/null +++ b/launcher/minecraft/mod/Resource.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" + +enum class ResourceType { + UNKNOWN, //!< Indicates an unspecified resource type. + ZIPFILE, //!< The resource is a zip file containing the resource's class files. + SINGLEFILE, //!< The resource is a single file (not a zip file). + FOLDER, //!< The resource is in a folder on the filesystem. + LITEMOD, //!< The resource is a litemod +}; + +/** General class for managed resources. It mirrors a file in disk, with some more info + * for display and house-keeping purposes. + * + * Subclass it to add additional data / behavior, such as Mods or Resource packs. + */ +class Resource : public QObject { + Q_OBJECT + Q_DISABLE_COPY(Resource) + public: + using Ptr = shared_qobject_ptr; + + Resource(QObject* parent = nullptr); + Resource(QFileInfo file_info); + ~Resource() override = default; + + void setFile(QFileInfo file_info); + void parseFile(); + + [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; } + [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } + [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } + [[nodiscard]] auto type() const -> ResourceType { return m_type; } + + [[nodiscard]] virtual auto name() const -> QString { return m_name; } + [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + + [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } + [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } + [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } + + void setResolving(bool resolving, int resolutionTicket) + { + m_is_resolving = resolving; + m_resolution_ticket = resolutionTicket; + } + + // Delete all files of this resource. + bool destroy(); + + protected: + /* The file corresponding to this resource. */ + QFileInfo m_file_info; + /* The cached date when this file was last changed. */ + QDateTime m_changed_date_time; + + /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ + QString m_internal_id; + /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ + QString m_name; + + /* The type of file we're dealing with. */ + ResourceType m_type = ResourceType::UNKNOWN; + + /* Used to keep trach of pending / concluded actions on the resource. */ + bool m_is_resolving = false; + bool m_is_resolved = false; + int m_resolution_ticket = 0; +}; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 60c54c4e..21c20b28 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -110,7 +110,7 @@ void EnsureMetadataTask::executeTask() } // Folders don't have metadata - if (mod->type() == Mod::MOD_FOLDER) { + if (mod->type() == ResourceType::FOLDER) { emitReady(mod); } } diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp index 7d78006b..22475abc 100644 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ b/launcher/ui/widgets/MCModInfoFrame.cpp @@ -23,7 +23,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) { - if (m.type() == m.MOD_FOLDER) + if (m.type() == ResourceType::FOLDER) { clear(); return; -- cgit From ec62d8e97334d3b5a30cea00858e7035468f3609 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:58:22 -0300 Subject: refactor: move general code from mod model to its own model This aims to continue decoupling other types of resources (e.g. resource packs, shader packs, etc) from mods, so that we don't have to continuously watch our backs for changes to one of them affecting the others. To do so, this creates a more general list model for resources, based on the mods one, that allows you to extend it with functionality for other resources. I had to do some template and preprocessor stuff to get around the QObject limitation of not allowing templated classes, so that's sadge :c On the other hand, I tried cleaning up most general-purpose code in the mod model, and added some documentation, because it looks nice :D Signed-off-by: flow --- launcher/CMakeLists.txt | 3 + launcher/minecraft/mod/ModFolderModel.cpp | 424 ++++----------------- launcher/minecraft/mod/ModFolderModel.h | 93 +---- launcher/minecraft/mod/ResourceFolderModel.cpp | 336 ++++++++++++++++ launcher/minecraft/mod/ResourceFolderModel.h | 274 +++++++++++++ launcher/minecraft/mod/tasks/BasicFolderLoadTask.h | 44 +++ launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 16 +- launcher/minecraft/mod/tasks/LocalModParseTask.h | 15 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 12 +- .../ui/pages/instance/ExternalResourcesPage.cpp | 8 +- 11 files changed, 778 insertions(+), 455 deletions(-) create mode 100644 launcher/minecraft/mod/ResourceFolderModel.cpp create mode 100644 launcher/minecraft/mod/ResourceFolderModel.h create mode 100644 launcher/minecraft/mod/tasks/BasicFolderLoadTask.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 70997e51..d1e0befd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -320,10 +320,13 @@ set(MINECRAFT_SOURCES minecraft/mod/ModFolderModel.cpp minecraft/mod/Resource.h minecraft/mod/Resource.cpp + minecraft/mod/ResourceFolderModel.h + minecraft/mod/ResourceFolderModel.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/tasks/BasicFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d4c5e819..597f9807 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -49,226 +49,91 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); - m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); - m_watcher = new QFileSystemWatcher(this); - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); } -void ModFolderModel::startWatching() +Task* ModFolderModel::createUpdateTask() { - if(is_watching) - return; + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); + m_first_folder_load = false; + return task; +} +void ModFolderModel::startWatching() +{ // Remove orphaned metadata next time m_first_folder_load = true; - - update(); - - // Watch the mods folder - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { - qDebug() << "Started watching " << m_dir.absolutePath(); - } else { - qDebug() << "Failed to start watching " << m_dir.absolutePath(); - } - - // Watch the mods index folder - is_watching = m_watcher->addPath(indexDir().absolutePath()); - if (is_watching) { - qDebug() << "Started watching " << indexDir().absolutePath(); - } else { - qDebug() << "Failed to start watching " << indexDir().absolutePath(); - } + ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } void ModFolderModel::stopWatching() { - if(!is_watching) - return; - - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { - qDebug() << "Stopped watching " << m_dir.absolutePath(); - } else { - qDebug() << "Failed to stop watching " << m_dir.absolutePath(); - } - - is_watching = !m_watcher->removePath(indexDir().absolutePath()); - if (!is_watching) { - qDebug() << "Stopped watching " << indexDir().absolutePath(); - } else { - qDebug() << "Failed to stop watching " << indexDir().absolutePath(); - } + ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } -bool ModFolderModel::update() +void ModFolderModel::onUpdateSucceeded() { - if (!isValid()) { - return false; - } - if(m_update) { - scheduled_update = true; - return true; - } - - auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); - m_first_folder_load = false; - - m_update = task->result(); + auto update_results = static_cast(m_current_update_task.get())->result(); - QThreadPool *threadPool = QThreadPool::globalInstance(); - connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); - - threadPool->start(task); - return true; -} + auto& new_mods = update_results->mods; -void ModFolderModel::finishUpdate() -{ #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto currentList = modsIndex.keys(); - QSet currentSet(currentList.begin(), currentList.end()); - auto & newMods = m_update->mods; - auto newList = newMods.keys(); - QSet newSet(newList.begin(), newList.end()); + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_mods.keys(); + QSet new_set(new_list.begin(), new_list.end()); #else - QSet currentSet = modsIndex.keys().toSet(); - auto& newMods = m_update->mods; - QSet newSet = newMods.keys().toSet(); + QSet current_set(m_resources_index.keys().toSet()); + QSet new_set(new_mods.keys().toSet()); #endif - // see if the kept mods changed in some way - { - QSet kept = currentSet; - kept.intersect(newSet); - for(auto& keptMod : kept) { - auto newMod = newMods[keptMod]; - auto row = modsIndex[keptMod]; - auto currentMod = mods[row]; - if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) { - // no significant change, ignore... - continue; - } - auto oldMod = mods[row]; - if(oldMod->isResolving()) { - activeTickets.remove(oldMod->resolutionTicket()); - } - - mods[row] = newMod; - resolveMod(mods[row]); - emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); - } - } - - // remove mods no longer present - { - QSet removed = currentSet; - QList removedRows; - removed.subtract(newSet); - for(auto & removedMod: removed) { - removedRows.append(modsIndex[removedMod]); - } - std::sort(removedRows.begin(), removedRows.end(), std::greater()); - for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) { - int removedIndex = *iter; - beginRemoveRows(QModelIndex(), removedIndex, removedIndex); - auto removedIter = mods.begin() + removedIndex; - if((*removedIter)->isResolving()) { - activeTickets.remove((*removedIter)->resolutionTicket()); - } - - mods.erase(removedIter); - endRemoveRows(); - } - } - - // add new mods to the end - { - QSet added = newSet; - added.subtract(currentSet); - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (added.size() > 0) { - beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); - for (auto& addedMod : added) { - mods.append(newMods[addedMod]); - resolveMod(mods.last()); - } - endInsertRows(); - } - } - - // update index - { - modsIndex.clear(); - int idx = 0; - for(auto mod: mods) { - modsIndex[mod->internal_id()] = idx; - idx++; - } - } - - m_update.reset(); + applyUpdates(current_set, new_set, new_mods); + + update_results.reset(); + m_current_update_task.reset(); emit updateFinished(); - if(scheduled_update) { - scheduled_update = false; + if(m_scheduled_update) { + m_scheduled_update = false; update(); } } -void ModFolderModel::resolveMod(Mod::Ptr m) +Task* ModFolderModel::createParseTask(Resource const& resource) { - if(!m->shouldResolve()) { - return; - } - - auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo()); - auto result = task->result(); - result->id = m->internal_id(); - activeTickets.insert(nextResolutionTicket, result); - m->setResolving(true, nextResolutionTicket); - nextResolutionTicket++; - QThreadPool *threadPool = QThreadPool::globalInstance(); - connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); - threadPool->start(task); + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } -void ModFolderModel::finishModParse(int token) +void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { - auto iter = activeTickets.find(token); - if(iter == activeTickets.end()) { + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) return; - } - auto result = *iter; - activeTickets.remove(token); - int row = modsIndex[result->id]; - auto mod = mods[row]; - mod->finishResolvingWithDetails(result->details); + + int row = m_resources_index[mod_id]; + + auto parse_task = *iter; + auto cast_task = static_cast(parse_task.get()); + + Q_ASSERT(cast_task->token() == ticket); + + auto resource = find(mod_id); + + auto result = cast_task->result(); + if (result && resource) + resource->finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); -} -void ModFolderModel::disableInteraction(bool disabled) -{ - if (interaction_disabled == disabled) { - return; - } - interaction_disabled = disabled; - if(size()) { - emit dataChanged(index(0), index(size() - 1)); - } + parse_task->deleteLater(); + m_active_parse_tasks.remove(ticket); } -void ModFolderModel::directoryChanged(QString path) -{ - update(); -} bool ModFolderModel::isValid() { @@ -277,104 +142,28 @@ bool ModFolderModel::isValid() auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList { - QList selected_mods; + QList selected_resources; for (auto i : indexes) { if(i.column() != 0) continue; - selected_mods.push_back(mods[i.row()]); + selected_resources.push_back(at(i.row())); } - return selected_mods; + return selected_resources; } -// FIXME: this does not take disabled mod (with extra .disable extension) into account... -bool ModFolderModel::installMod(const QString &filename) +auto ModFolderModel::allMods() -> QList { - if(interaction_disabled) { - return false; - } - - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - auto originalPath = FS::NormalizePath(filename); - QFileInfo fileinfo(originalPath); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath; - return false; - } - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - Mod installedMod(fileinfo); - if (!installedMod.valid()) - { - qDebug() << originalPath << "is not a valid mod. Ignoring it."; - return false; - } - - auto type = installedMod.type(); - if (type == Mod::MOD_UNKNOWN) - { - qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it."; - return false; - } - - auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName())); - if(originalPath == newpath) - { - qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense..."; - return false; - } + QList mods; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled"))) - { - if(!QFile::remove(newpath)) - { - // FIXME: report error in a user-visible way - qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; - return false; - } - qDebug() << newpath << "has been deleted."; - } - if (!QFile::copy(fileinfo.filePath(), newpath)) - { - qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; - // FIXME: report error in a user-visible way - return false; - } - FS::updateTimestamp(newpath); - QFileInfo newpathInfo(newpath); - installedMod.repath(newpathInfo); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - if(QFile::exists(newpath)) - { - qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath; - return false; - } + for (auto res : m_resources) + mods.append(static_cast(res.get())); - if (!FS::copy(from, newpath)()) - { - qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed."; - return false; - } - QFileInfo newpathInfo(newpath); - installedMod.repath(newpathInfo); - update(); - return true; - } - return false; + return mods; } bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) { - for(auto mod : allMods()){ if(mod->fileinfo().fileName() == filename){ auto index_dir = indexDir(); @@ -388,7 +177,7 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) { - if(interaction_disabled) { + if(!m_can_interact) { return false; } @@ -407,7 +196,7 @@ bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusActio bool ModFolderModel::deleteMods(const QModelIndexList& indexes) { - if(interaction_disabled) { + if(!m_can_interact) { return false; } @@ -419,7 +208,7 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) if(i.column() != 0) { continue; } - auto m = mods[i.row()]; + auto m = at(i.row()); auto index_dir = indexDir(); m->destroy(index_dir); } @@ -433,48 +222,45 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const QVariant ModFolderModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) - return QVariant(); + if (!validateIndex(index)) + return {}; int row = index.row(); int column = index.column(); - if (row < 0 || row >= mods.size()) - return QVariant(); - switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: - return mods[row]->name(); + return m_resources[row]->name(); case VersionColumn: { - switch(mods[row]->type()) { - case Mod::MOD_FOLDER: + switch(m_resources[row]->type()) { + case ResourceType::FOLDER: return tr("Folder"); - case Mod::MOD_SINGLEFILE: + case ResourceType::SINGLEFILE: return tr("File"); default: break; } - return mods[row]->version(); + return at(row)->version(); } case DateColumn: - return mods[row]->dateTimeChanged(); + return m_resources[row]->dateTimeChanged(); default: return QVariant(); } case Qt::ToolTipRole: - return mods[row]->internal_id(); + return m_resources[row]->internal_id(); case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -499,11 +285,11 @@ bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, in bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) { - if(row < 0 || row >= mods.size()) { + if(row < 0 || row >= m_resources.size()) { return false; } - auto &mod = mods[row]; + auto mod = at(row); bool desiredStatus; switch(action) { case Enable: @@ -528,12 +314,12 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio return false; } auto newId = mod->internal_id(); - if(modsIndex.contains(newId)) { + if(m_resources_index.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? } - modsIndex.remove(oldId); - modsIndex[newId] = row; + m_resources_index.remove(oldId); + m_resources_index[newId] = row; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); return true; } @@ -577,65 +363,3 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return QVariant(); } -Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - auto flags = defaultFlags; - if(interaction_disabled) { - flags &= ~Qt::ItemIsDropEnabled; - } - else - { - flags |= Qt::ItemIsDropEnabled; - if(index.isValid()) { - flags |= Qt::ItemIsUserCheckable; - } - } - return flags; -} - -Qt::DropActions ModFolderModel::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList ModFolderModel::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) -{ - if (action == Qt::IgnoreAction) - { - return true; - } - - // check if the action is supported - if (!data || !(action & supportedDropActions())) - { - return false; - } - - // files dropped from outside? - if (data->hasUrls()) - { - auto urls = data->urls(); - for (auto url : urls) - { - // only local files may be dropped... - if (!url.isLocalFile()) - { - continue; - } - // TODO: implement not only copy, but also move - // FIXME: handle errors here - installMod(url.toLocalFile()); - } - return true; - } - return false; -} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 3d6efac3..a90457d5 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -44,6 +44,7 @@ #include #include "Mod.h" +#include "ResourceFolderModel.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h" @@ -56,7 +57,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class ModFolderModel : public QAbstractListModel +class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: @@ -75,48 +76,18 @@ public: }; ModFolderModel(const QString &dir, bool is_indexed = false); - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - Qt::DropActions supportedDropActions() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - /// flags, mostly to support drag&drop - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - QStringList mimeTypes() const override; - bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent) const override; - virtual int rowCount(const QModelIndex &) const override - { - return size(); - } + [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource const&) override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - virtual int columnCount(const QModelIndex &parent) const override; - - size_t size() const - { - return mods.size(); - } - ; - bool empty() const - { - return size() == 0; - } - Mod& operator[](size_t index) - { - return *mods[index]; - } - const Mod& at(size_t index) const - { - return *mods.at(index); - } - - /// Reloads the mod list and returns true if the list changed. - bool update(); - - /** - * Adds the given mod to the list at the given index - if the list supports custom ordering - */ - bool installMod(const QString& filename); + // Alias for old code, consider those deprecated and don't use in new code :gun: + bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } + void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); } bool uninstallMod(const QString& filename, bool preserve_metadata = false); @@ -126,55 +97,27 @@ public: /// Enable or disable listed mods bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); - void startWatching(); - void stopWatching(); - bool isValid(); - QDir& dir() - { - return m_dir; - } - - QDir indexDir() - { - return { QString("%1/.index").arg(dir().absolutePath()) }; - } + void startWatching(); + void stopWatching(); - const QList& allMods() - { - return mods; - } + QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } auto selectedMods(QModelIndexList& indexes) -> QList; + auto allMods() -> QList; -public slots: - void disableInteraction(bool disabled); + RESOURCE_HELPERS(Mod) private slots: - void directoryChanged(QString path); - void finishUpdate(); - void finishModParse(int token); - -signals: - void updateFinished(); + void onUpdateSucceeded() override; + void onParseSucceeded(int ticket, QString resource_id) override; private: - void resolveMod(Mod::Ptr m); bool setModStatus(int index, ModStatusAction action); protected: - QFileSystemWatcher *m_watcher; - bool is_watching = false; - ModFolderLoadTask::ResultPtr m_update; - bool scheduled_update = false; - bool interaction_disabled = false; - QDir m_dir; bool m_is_indexed; bool m_first_folder_load = true; - QMap modsIndex; - QMap activeTickets; - int nextResolutionTicket = 0; - QList mods; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp new file mode 100644 index 00000000..4867a8c2 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -0,0 +1,336 @@ +#include "ResourceFolderModel.h" + +#include +#include +#include +#include + +#include "FileSystem.h" + +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" + +#include "tasks/Task.h" + +ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) +{ + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); +} + +bool ResourceFolderModel::startWatching(const QStringList paths) +{ + if (m_is_watching) + return false; + + update(); + + auto couldnt_be_watched = m_watcher.addPaths(paths); + for (auto path : paths) { + if (couldnt_be_watched.contains(path)) + qDebug() << "Failed to start watching " << path; + else + qDebug() << "Started watching " << path; + } + + m_is_watching = !m_is_watching; + return m_is_watching; +} + +bool ResourceFolderModel::stopWatching(const QStringList paths) +{ + if (!m_is_watching) + return false; + + auto couldnt_be_stopped = m_watcher.removePaths(paths); + for (auto path : paths) { + if (couldnt_be_stopped.contains(path)) + qDebug() << "Failed to stop watching " << path; + else + qDebug() << "Stopped watching " << path; + } + + m_is_watching = !m_is_watching; + return !m_is_watching; +} + +bool ResourceFolderModel::installResource(QString original_path) +{ + if (!m_can_interact) { + return false; + } + + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + original_path = FS::NormalizePath(original_path); + QFileInfo file_info(original_path); + + if (!file_info.exists() || !file_info.isReadable()) { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; + return false; + } + qDebug() << "Installing: " << file_info.absoluteFilePath(); + + Resource resource(file_info); + if (!resource.valid()) { + qWarning() << original_path << "is not a valid resource. Ignoring it."; + return false; + } + + auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); + if (original_path == new_path) { + qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; + return false; + } + + switch (resource.type()) { + case ResourceType::SINGLEFILE: + case ResourceType::ZIPFILE: + case ResourceType::LITEMOD: { + if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { + if (!QFile::remove(new_path)) { + qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; + return false; + } + qDebug() << new_path << "has been deleted."; + } + + if (!QFile::copy(original_path, new_path)) { + qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; + return false; + } + + FS::updateTimestamp(new_path); + + QFileInfo new_path_file_info(new_path); + resource.setFile(new_path_file_info); + + update(); + + return true; + } + case ResourceType::FOLDER: { + if (QFile::exists(new_path)) { + qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; + return false; + } + + if (!FS::copy(original_path, new_path)()) { + qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; + return false; + } + + QFileInfo newpathInfo(new_path); + resource.setFile(newpathInfo); + + update(); + + return true; + } + default: + break; + } + return false; +} + +bool ResourceFolderModel::uninstallResource(QString file_name) +{ + for (auto resource : m_resources) { + if (resource->fileinfo().fileName() == file_name) + return resource->destroy(); + } + return false; +} + +bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) +{ + if (!m_can_interact) + return false; + + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) { + if (i.column() != 0) { + continue; + } + + auto resource = m_resources.at(i.row()); + resource->destroy(); + } + return true; +} + +bool ResourceFolderModel::update() +{ + // Already updating, so we schedule a future update and return. + if (m_current_update_task) { + m_scheduled_update = true; + return false; + } + + m_current_update_task.reset(createUpdateTask()); + + connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); + + auto* thread_pool = QThreadPool::globalInstance(); + thread_pool->start(m_current_update_task.get()); + + return true; +} + +void ResourceFolderModel::resolveResource(Resource::Ptr res) +{ + if (!res->shouldResolve()) { + return; + } + + auto task = createParseTask(*res); + + m_ticket_mutex.lock(); + int ticket = m_next_resolution_ticket; + m_next_resolution_ticket += 1; + m_ticket_mutex.unlock(); + + res->setResolving(true, ticket); + m_active_parse_tasks.insert(ticket, task); + + connect(task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + connect(task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + + auto* thread_pool = QThreadPool::globalInstance(); + thread_pool->start(task); +} + +void ResourceFolderModel::onUpdateSucceeded() +{ + auto update_results = static_cast(m_current_update_task.get())->result(); + + auto& new_resources = update_results->resources; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_resources.keys(); + QSet new_set(new_list.begin(), new_list.end()); +#else + QSet current_set(m_resources_index.keys().toSet()); + QSet new_set(new_resources.keys().toSet()); +#endif + + applyUpdates(current_set, new_set, new_resources); + + update_results.reset(); + m_current_update_task->deleteLater(); + m_current_update_task.reset(); + + emit updateFinished(); + + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } +} + +void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) + return; + + (*iter)->deleteLater(); + m_active_parse_tasks.remove(ticket); + + int row = m_resources_index[resource_id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +Task* ResourceFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir); +} + +void ResourceFolderModel::directoryChanged(QString path) +{ + update(); +} + +Qt::DropActions ResourceFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other resource lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags; + if (!m_can_interact) { + flags &= ~Qt::ItemIsDropEnabled; + } else { + flags |= Qt::ItemIsDropEnabled; + if (index.isValid()) { + flags |= Qt::ItemIsUserCheckable; + } + } + return flags; +} + +QStringList ResourceFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) { + continue; + } + // TODO: implement not only copy, but also move + // FIXME: handle errors here + installResource(url.toLocalFile()); + } + return true; + } + return false; +} + +bool ResourceFolderModel::validateIndex(const QModelIndex& index) const +{ + if (!index.isValid()) + return false; + + size_t row = index.row(); + if (row < 0 || row >= size()) + return false; + + return true; +} + +void ResourceFolderModel::enableInteraction(bool enabled) +{ + if (m_can_interact == enabled) + return; + + m_can_interact = enabled; + if (size()) + emit dataChanged(index(0), index(size() - 1)); +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h new file mode 100644 index 00000000..31fd7414 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -0,0 +1,274 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Resource.h" + +#include "tasks/Task.h" + +class QRunnable; + +/** A basic model for external resources. + * + * To implement one such model, you need to implement, at the very minimum: + * - columnCount: The number of columns in your model. + * - data: How the model data is displayed and accessed. + * - headerData: Display properties of the header. + */ +class ResourceFolderModel : public QAbstractListModel { + Q_OBJECT + public: + ResourceFolderModel(QDir, QObject* parent = nullptr); + + /** Starts watching the paths for changes. + * + * Returns whether starting to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool startWatching(const QStringList paths); + + /** Stops watching the paths for changes. + * + * Returns whether stopping to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool stopWatching(const QStringList paths); + + /** Given a path in the system, install that resource, moving it to its place in the + * instance file hierarchy. + * + * Returns whether the installation was succcessful. + */ + virtual bool installResource(QString path); + + /** Uninstall (i.e. remove all data about it) a resource, given its file name. + * + * Returns whether the removal was successful. + */ + virtual bool uninstallResource(QString file_name); + + virtual bool deleteResources(const QModelIndexList&); + + /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ + virtual bool update(); + + /** Creates a new parse task, if needed, for 'res' and start it.*/ + virtual void resolveResource(Resource::Ptr res); + + [[nodiscard]] size_t size() const { return m_resources.size(); }; + [[nodiscard]] bool empty() const { return size() == 0; } + + [[nodiscard]] QDir const& dir() const { return m_dir; } + + /* Qt behavior */ + + [[nodiscard]] int rowCount(const QModelIndex&) const override { return size(); } + [[nodiscard]] int columnCount(const QModelIndex&) const override = 0; + + [[nodiscard]] Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override; + [[nodiscard]] QStringList mimeTypes() const override; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + + [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + + [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return false; }; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override = 0; + + public slots: + void enableInteraction(bool enabled); + + signals: + void updateFinished(); + + protected: + /** This creates a new update task to be executed by update(). + * + * The task should load and parse all resources necessary, and provide a way of accessing such results. + * + * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. + * If such work is needed, try using it in the Task create by createParseTask() instead! + */ + [[nodiscard]] virtual Task* createUpdateTask(); + + /** This creates a new parse task to be executed by onUpdateSucceeded(). + * + * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed + * in the background, so it slowly updates the UI as tasks get done. + */ + [[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; }; + + /** Standard implementation of the model update logic. + * + * It uses set operations to find differences between the current state and the updated state, + * to act only on those disparities. + * + * The implementation is at the end of this header. + */ + template + void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + + protected slots: + void directoryChanged(QString); + + /** Called when the update task is successful. + * + * This usually calls static_cast on the specific Task type returned by createUpdateTask, + * so care must be taken in such cases. + * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro dissalows that). + */ + virtual void onUpdateSucceeded(); + virtual void onUpdateFailed() {} + + /** Called when the parse task with the given ticket is successful. + * + * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass + * if the resource is complex and has more stuff to parse. + */ + virtual void onParseSucceeded(int ticket, QString resource_id); + virtual void onParseFailed(int ticket, QString resource_id) {} + + protected: + bool m_can_interact = true; + + QDir m_dir; + QFileSystemWatcher m_watcher; + bool m_is_watching = false; + + Task::Ptr m_current_update_task = nullptr; + bool m_scheduled_update = false; + + QList m_resources; + + // Represents the relationship between a resource's internal ID and it's row position on the model. + QMap m_resources_index; + + QMap m_active_parse_tasks; + int m_next_resolution_ticket = 0; + QMutex m_ticket_mutex; +}; + +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + [[nodiscard]] T* operator[](size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] T* at(size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] const T* at(size_t index) const \ + { \ + return static_cast(m_resources.at(index).get()); \ + } \ + [[nodiscard]] T* first() \ + { \ + return static_cast(m_resources.first().get()); \ + } \ + [[nodiscard]] T* last() \ + { \ + return static_cast(m_resources.last().get()); \ + } \ + [[nodiscard]] T* find(QString id) \ + { \ + auto iter = std::find_if(m_resources.begin(), m_resources.end(), [&](Resource::Ptr r) { return r->internal_id() == id; }); \ + if (iter == m_resources.end()) \ + return nullptr; \ + return static_cast((*iter).get()); \ + } + +/* Template definition to avoid some code duplication */ +template +void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) +{ + // see if the kept resources changed in some way + { + QSet kept_set = current_set; + kept_set.intersect(new_set); + + for (auto& kept : kept_set) { + auto row = m_resources_index[kept]; + + auto new_resource = new_resources[kept]; + auto current_resource = m_resources[row]; + + if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { + // no significant change, ignore... + continue; + } + + // If the resource is resolving, but something about it changed, we don't want to + // continue the resolving. + if (current_resource->isResolving()) { + m_active_parse_tasks.remove(current_resource->resolutionTicket()); + } + + m_resources[row] = new_resource; + resolveResource(new_resource); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet removed_set = current_set; + removed_set.subtract(new_set); + + QList removed_rows; + for (auto& removed : removed_set) + removed_rows.append(m_resources_index[removed]); + + std::sort(removed_rows.begin(), removed_rows.end()); + + for (auto& removed_index : removed_rows) { + beginRemoveRows(QModelIndex(), removed_index, removed_index); + + auto removed_it = m_resources.begin() + removed_index; + if ((*removed_it)->isResolving()) { + m_active_parse_tasks.remove((*removed_it)->resolutionTicket()); + } + + m_resources.erase(removed_it); + + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet added_set = new_set; + added_set.subtract(current_set); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added_set.size() > 0) { + beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1); + + for (auto& added : added_set) { + auto res = new_resources[added]; + m_resources.append(res); + resolveResource(res); + } + + endInsertRows(); + } + } + + // update index + { + m_resources_index.clear(); + int idx = 0; + for (auto mod : m_resources) { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + } +} diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h new file mode 100644 index 00000000..0fd5c292 --- /dev/null +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include + +#include "minecraft/mod/Resource.h" + +#include "tasks/Task.h" + +/** Very simple task that just loads a folder's contents directly. + */ +class BasicFolderLoadTask : public Task +{ + Q_OBJECT +public: + struct Result { + QMap resources; + }; + using ResultPtr = std::shared_ptr; + + [[nodiscard]] ResultPtr result() const { + return m_result; + } + +public: + BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {} + void executeTask() override + { + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) { + auto resource = new Resource(entry); + m_result->resources.insert(resource->internal_id(), resource); + } + + emitSucceeded(); + } + +private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 1519f49d..fe3716ce 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -338,13 +338,13 @@ std::shared_ptr ReadLiteModInfo(QByteArray contents) } -LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile): + Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) -{ -} +{} void LocalModParseTask::processAsZip() { @@ -497,21 +497,21 @@ void LocalModParseTask::processAsLitemod() zip.close(); } -void LocalModParseTask::run() +void LocalModParseTask::executeTask() { switch(m_type) { - case Mod::MOD_ZIPFILE: + case ResourceType::ZIPFILE: processAsZip(); break; - case Mod::MOD_FOLDER: + case ResourceType::FOLDER: processAsFolder(); break; - case Mod::MOD_LITEMOD: + case ResourceType::LITEMOD: processAsLitemod(); break; default: break; } - emit finished(m_token); + emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index ed92394c..dbecb449 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -2,17 +2,17 @@ #include #include -#include #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModDetails.h" -class LocalModParseTask : public QObject, public QRunnable +#include "tasks/Task.h" + +class LocalModParseTask : public Task { Q_OBJECT public: struct Result { - QString id; std::shared_ptr details; }; using ResultPtr = std::shared_ptr; @@ -20,11 +20,10 @@ public: return m_result; } - LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile); - void run(); + LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile); + void executeTask() override; -signals: - void finished(int token); + [[nodiscard]] int token() const { return m_token; } private: void processAsZip(); @@ -33,7 +32,7 @@ private: private: int m_token; - Mod::ModType m_type; + ResourceType m_type; QFileInfo m_modFile; ResultPtr m_result; }; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 015ead80..e8180c11 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -38,11 +38,11 @@ #include "minecraft/mod/MetadataHandler.h" -ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan) - : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) +ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) + : Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) {} -void ModFolderLoadTask::run() +void ModFolderLoadTask::executeTask() { if (m_is_indexed) { // Read metadata first @@ -96,7 +96,7 @@ void ModFolderLoadTask::run() } } - emit succeeded(); + emitSucceeded(); } void ModFolderLoadTask::getFromMetadata() diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 1f2015d2..86f3f67f 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -42,8 +42,9 @@ #include #include #include "minecraft/mod/Mod.h" +#include "tasks/Task.h" -class ModFolderLoadTask : public QObject, public QRunnable +class ModFolderLoadTask : public Task { Q_OBJECT public: @@ -56,16 +57,15 @@ public: } public: - ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false); - void run(); -signals: - void succeeded(); + ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false); + + void executeTask() override; private: void getFromMetadata(); private: - QDir& m_mods_dir, m_index_dir; + QDir m_mods_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; ResultPtr m_result; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 39fbe3e2..da7c4af0 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -32,12 +32,12 @@ class SortProxy : public QSortFilterProxyModel { const auto& mod = model->at(source_row); - if (filterRegularExpression().match(mod.name()).hasMatch()) + if (filterRegularExpression().match(mod->name()).hasMatch()) return true; - if (filterRegularExpression().match(mod.description()).hasMatch()) + if (filterRegularExpression().match(mod->description()).hasMatch()) return true; - for (auto& author : mod.authors()) { + for (auto& author : mod->authors()) { if (filterRegularExpression().match(author).hasMatch()) { return true; } @@ -292,6 +292,6 @@ void ExternalResourcesPage::current(const QModelIndex& current, const QModelInde auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - Mod& m = m_model->operator[](row); + Mod& m = *m_model->operator[](row); ui->frame->updateWithMod(m); } -- cgit From af2cf2734da211d443b3046fb0f9733f101d9d9d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 12:56:38 -0300 Subject: refactor: move things around in the mod model Makes the method order in the cpp file the same as in the header file. Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel.cpp | 346 +++++++++++++++--------------- launcher/minecraft/mod/ModFolderModel.h | 5 +- 2 files changed, 175 insertions(+), 176 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 597f9807..8ab60413 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -54,6 +54,113 @@ ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFo FS::ensureFolderPathExists(m_dir.absolutePath()); } +QVariant ModFolderModel::data(const QModelIndex &index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return m_resources[row]->name(); + case VersionColumn: { + switch(m_resources[row]->type()) { + case ResourceType::FOLDER: + return tr("Folder"); + case ResourceType::SINGLEFILE: + return tr("File"); + default: + break; + } + return at(row)->version(); + } + case DateColumn: + return m_resources[row]->dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return m_resources[row]->internal_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + return setModStatus(index.row(), Toggle); + } + return false; +} + +QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +int ModFolderModel::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + Task* ModFolderModel::createUpdateTask() { auto index_dir = indexDir(); @@ -62,6 +169,50 @@ Task* ModFolderModel::createUpdateTask() return task; } +Task* ModFolderModel::createParseTask(Resource const& resource) +{ + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); +} + +bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) +{ + for(auto mod : allMods()){ + if(mod->fileinfo().fileName() == filename){ + auto index_dir = indexDir(); + mod->destroy(index_dir, preserve_metadata); + return true; + } + } + + return false; +} + +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) +{ + if(!m_can_interact) { + return false; + } + + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + if(i.column() != 0) { + continue; + } + auto m = at(i.row()); + auto index_dir = indexDir(); + m->destroy(index_dir); + } + return true; +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + void ModFolderModel::startWatching() { // Remove orphaned metadata next time @@ -74,6 +225,28 @@ void ModFolderModel::stopWatching() ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList +{ + QList selected_resources; + for (auto i : indexes) { + if(i.column() != 0) + continue; + + selected_resources.push_back(at(i.row())); + } + return selected_resources; +} + +auto ModFolderModel::allMods() -> QList +{ + QList mods; + + for (auto res : m_resources) + mods.append(static_cast(res.get())); + + return mods; +} + void ModFolderModel::onUpdateSucceeded() { auto update_results = static_cast(m_current_update_task.get())->result(); @@ -104,11 +277,6 @@ void ModFolderModel::onUpdateSucceeded() } } -Task* ModFolderModel::createParseTask(Resource const& resource) -{ - return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); -} - void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { auto iter = m_active_parse_tasks.constFind(ticket); @@ -135,46 +303,6 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) } -bool ModFolderModel::isValid() -{ - return m_dir.exists() && m_dir.isReadable(); -} - -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList -{ - QList selected_resources; - for (auto i : indexes) { - if(i.column() != 0) - continue; - - selected_resources.push_back(at(i.row())); - } - return selected_resources; -} - -auto ModFolderModel::allMods() -> QList -{ - QList mods; - - for (auto res : m_resources) - mods.append(static_cast(res.get())); - - return mods; -} - -bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) -{ - for(auto mod : allMods()){ - if(mod->fileinfo().fileName() == filename){ - auto index_dir = indexDir(); - mod->destroy(index_dir, preserve_metadata); - return true; - } - } - - return false; -} - bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) { if(!m_can_interact) { @@ -194,95 +322,6 @@ bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusActio return true; } -bool ModFolderModel::deleteMods(const QModelIndexList& indexes) -{ - if(!m_can_interact) { - return false; - } - - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - if(i.column() != 0) { - continue; - } - auto m = at(i.row()); - auto index_dir = indexDir(); - m->destroy(index_dir); - } - return true; -} - -int ModFolderModel::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant ModFolderModel::data(const QModelIndex &index, int role) const -{ - if (!validateIndex(index)) - return {}; - - int row = index.row(); - int column = index.column(); - - switch (role) - { - case Qt::DisplayRole: - switch (column) - { - case NameColumn: - return m_resources[row]->name(); - case VersionColumn: { - switch(m_resources[row]->type()) { - case ResourceType::FOLDER: - return tr("Folder"); - case ResourceType::SINGLEFILE: - return tr("File"); - default: - break; - } - return at(row)->version(); - } - case DateColumn: - return m_resources[row]->dateTimeChanged(); - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return m_resources[row]->internal_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) - { - return false; - } - - if (role == Qt::CheckStateRole) - { - return setModStatus(index.row(), Toggle); - } - return false; -} - bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) { if(row < 0 || row >= m_resources.size()) { @@ -324,42 +363,3 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio return true; } -QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - switch (role) - { - case Qt::DisplayRole: - switch (section) - { - case ActiveColumn: - return QString(); - case NameColumn: - return tr("Name"); - case VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index a90457d5..ea9f0000 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -85,15 +85,14 @@ public: [[nodiscard]] Task* createUpdateTask() override; [[nodiscard]] Task* createParseTask(Resource const&) override; - // Alias for old code, consider those deprecated and don't use in new code :gun: bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } - void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); } - bool uninstallMod(const QString& filename, bool preserve_metadata = false); /// Deletes all the selected mods bool deleteMods(const QModelIndexList &indexes); + void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); } + /// Enable or disable listed mods bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); -- cgit From 1e2f0ab3083f002071938275a97b13c0c4633e64 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 10 Aug 2022 14:42:24 -0300 Subject: refactor: move more tied logic to model and move logic to the resources This moves the QSortFilterProxyModel to the resource model files, acessible via a factory method, and moves the sorting and filtering to the objects themselves, decoupling the code a bit. This also adds a basic implementation of methods in the ResourceFolderModel, simplifying the process of constructing a new model from it. Signed-off-by: flow --- launcher/minecraft/mod/Mod.cpp | 47 ++++++++++ launcher/minecraft/mod/Mod.h | 3 + launcher/minecraft/mod/ModFolderModel.cpp | 9 +- launcher/minecraft/mod/ModFolderModel.h | 6 +- launcher/minecraft/mod/Resource.cpp | 39 +++++++++ launcher/minecraft/mod/Resource.h | 21 +++++ launcher/minecraft/mod/ResourceFolderModel.cpp | 114 ++++++++++++++++++++++++- launcher/minecraft/mod/ResourceFolderModel.h | 42 +++++++-- 8 files changed, 265 insertions(+), 16 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index f28fd32a..ed91d999 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -39,8 +39,10 @@ #include #include #include +#include #include "MetadataHandler.h" +#include "Version.h" namespace { @@ -111,6 +113,51 @@ void Mod::setMetadata(const Metadata::ModStruct& metadata) } } +std::pair Mod::compare(const Resource& other, SortType type) const +{ + auto cast_other = dynamic_cast(&other); + if (!cast_other) + return Resource::compare(other, type); + + switch (type) { + default: + case SortType::ENABLED: + if (enabled() && !cast_other->enabled()) + return { 1, type == SortType::ENABLED }; + if (!enabled() && cast_other->enabled()) + return { -1, type == SortType::ENABLED }; + case SortType::NAME: + case SortType::DATE: { + auto res = Resource::compare(other, type); + if (res.first != 0) + return res; + } + case SortType::VERSION: { + auto this_ver = Version(version()); + auto other_ver = Version(cast_other->version()); + if (this_ver > other_ver) + return { 1, type == SortType::VERSION }; + if (this_ver < other_ver) + return { -1, type == SortType::VERSION }; + } + } + return { 0, false }; +} + +bool Mod::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + for (auto& author : authors()) { + if (filter.match(author).hasMatch()) { + return true; + } + } + + return Resource::applyFilter(filter); +} + auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool { if (!preserve_metadata) { diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 313c478c..25ac88c9 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -70,6 +70,9 @@ public: auto enable(bool value) -> bool; + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 8ab60413..9b8c58bc 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -52,6 +52,7 @@ ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; } QVariant ModFolderModel::data(const QModelIndex &index, int role) const @@ -213,16 +214,16 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } -void ModFolderModel::startWatching() +bool ModFolderModel::startWatching() { // Remove orphaned metadata next time m_first_folder_load = true; - ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); + return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } -void ModFolderModel::stopWatching() +bool ModFolderModel::stopWatching() { - ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); + return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index ea9f0000..b1f30710 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -91,15 +91,13 @@ public: /// Deletes all the selected mods bool deleteMods(const QModelIndexList &indexes); - void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); } - /// Enable or disable listed mods bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); bool isValid(); - void startWatching(); - void stopWatching(); + bool startWatching() override; + bool stopWatching() override; QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 8771a20f..c58df3d8 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -1,5 +1,7 @@ #include "Resource.h" +#include + #include "FileSystem.h" Resource::Resource(QObject* parent) : QObject(parent) {} @@ -46,6 +48,43 @@ void Resource::parseFile() m_changed_date_time = m_file_info.lastModified(); } +static void removeThePrefix(QString& string) +{ + QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); + string.remove(regex); + string = string.trimmed(); +} + +std::pair Resource::compare(const Resource& other, SortType type) const +{ + switch (type) { + default: + case SortType::NAME: { + QString this_name{ name() }; + QString other_name{ other.name() }; + + removeThePrefix(this_name); + removeThePrefix(other_name); + + auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); + if (compare_result != 0) + return { compare_result, type == SortType::NAME }; + } + case SortType::DATE: + if (dateTimeChanged() > other.dateTimeChanged()) + return { 1, type == SortType::DATE }; + if (dateTimeChanged() < other.dateTimeChanged()) + return { -1, type == SortType::DATE }; + } + + return { 0, false }; +} + +bool Resource::applyFilter(QRegularExpression filter) const +{ + return filter.match(name()).hasMatch(); +} + bool Resource::destroy() { m_type = ResourceType::UNKNOWN; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index c348c7e2..68663ab0 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -14,6 +14,13 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; +enum class SortType { + NAME, + DATE, + VERSION, + ENABLED, +}; + /** General class for managed resources. It mirrors a file in disk, with some more info * for display and house-keeping purposes. * @@ -40,6 +47,20 @@ class Resource : public QObject { [[nodiscard]] virtual auto name() const -> QString { return m_name; } [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: + * > 0: 'this' comes after 'other' + * = 0: 'this' is equal to 'other' + * < 0: 'this' comes before 'other' + * + * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'. + */ + [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair; + + /** Returns whether the given filter should filter out 'this' (false), + * or if such filter includes the Resource (true). + */ + [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const; + [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 4867a8c2..982915e2 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -170,8 +170,11 @@ bool ResourceFolderModel::update() } m_current_update_task.reset(createUpdateTask()); + if (!m_current_update_task) + return false; - connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, + Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); auto* thread_pool = QThreadPool::globalInstance(); @@ -187,6 +190,8 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) } auto task = createParseTask(*res); + if (!task) + return; m_ticket_mutex.lock(); int ticket = m_next_resolution_ticket; @@ -196,8 +201,10 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) res->setResolving(true, ticket); m_active_parse_tasks.insert(ticket, task); - connect(task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); - connect(task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + connect( + task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + connect( + task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); auto* thread_pool = QThreadPool::globalInstance(); thread_pool->start(task); @@ -325,6 +332,71 @@ bool ResourceFolderModel::validateIndex(const QModelIndex& index) const return true; } +QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NAME_COLUMN: + return m_resources[row]->name(); + case DATE_COLUMN: + return m_resources[row]->dateTimeChanged(); + default: + return {}; + } + case Qt::ToolTipRole: + return m_resources[row]->internal_id(); + default: + return {}; + } +} + +QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case NAME_COLUMN: + return tr("Name"); + case DATE_COLUMN: + return tr("Last modified"); + default: + return {}; + } + case Qt::ToolTipRole: { + switch (section) { + case NAME_COLUMN: + return tr("The name of the resource."); + case DATE_COLUMN: + return tr("The date and time this resource was last changed (or added)."); + default: + return {}; + } + } + default: + break; + } + + return {}; +} + +QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) +{ + return new ProxyModel(parent); +} + +SortType ResourceFolderModel::columnToSortKey(size_t column) const +{ + Q_ASSERT(m_column_sort_keys.size() == columnCount()); + return m_column_sort_keys.at(column); +} + void ResourceFolderModel::enableInteraction(bool enabled) { if (m_can_interact == enabled) @@ -334,3 +406,39 @@ void ResourceFolderModel::enableInteraction(bool enabled) if (size()) emit dataChanged(index(0), index(size() - 1)); } + +/* Standard Proxy Model for createFilterProxyModel */ +[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + auto* model = qobject_cast(sourceModel()); + if (!model) + return true; + + const auto& resource = model->at(source_row); + + return resource.applyFilter(filterRegularExpression()); +} + +[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +{ + auto* model = qobject_cast(sourceModel()); + if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and + // proceed. + + auto column_sort_key = model->columnToSortKey(source_left.column()); + auto const& resource_left = model->at(source_left.row()); + auto const& resource_right = model->at(source_right.row()); + + auto compare_result = resource_left.compare(resource_right, column_sort_key); + if (compare_result.first == 0) + return QSortFilterProxyModel::lessThan(source_left, source_right); + + if (compare_result.second || sortOrder() != Qt::DescendingOrder) + return (compare_result.first < 0); + return (compare_result.first > 0); +} + diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 31fd7414..2ccf14f0 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -5,12 +5,13 @@ #include #include #include +#include #include "Resource.h" #include "tasks/Task.h" -class QRunnable; +class QSortFilterProxyModel; /** A basic model for external resources. * @@ -38,6 +39,10 @@ class ResourceFolderModel : public QAbstractListModel { */ bool stopWatching(const QStringList paths); + /* Helper methods for subclasses, using a predetermined list of paths. */ + virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); }; + virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); }; + /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. * @@ -61,13 +66,18 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] bool empty() const { return size() == 0; } + [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } + [[nodiscard]] QList const& all() const { return m_resources; } [[nodiscard]] QDir const& dir() const { return m_dir; } /* Qt behavior */ - [[nodiscard]] int rowCount(const QModelIndex&) const override { return size(); } - [[nodiscard]] int columnCount(const QModelIndex&) const override = 0; + /* Basic columns */ + enum Columns { NAME_COLUMN = 0, DATE_COLUMN, NUM_COLUMNS }; + + [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); } + [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; }; [[nodiscard]] Qt::DropActions supportedDropActions() const override; @@ -78,13 +88,31 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool validateIndex(const QModelIndex& index) const; - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0; + [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return false; }; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override = 0; + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + /** This creates a proxy model to filter / sort the model for a UI. + * + * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! + */ + QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); + + [[nodiscard]] SortType columnToSortKey(size_t column) const; + + class ProxyModel : public QSortFilterProxyModel { + public: + explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} + + protected: + [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + }; public slots: void enableInteraction(bool enabled); + void disableInteraction(bool disabled) { enableInteraction(!disabled); } signals: void updateFinished(); @@ -137,6 +165,10 @@ class ResourceFolderModel : public QAbstractListModel { virtual void onParseFailed(int ticket, QString resource_id) {} protected: + // Represents the relationship between a column's index (represented by the list index), and it's sorting key. + // As such, the order in with they appear is very important! + QList m_column_sort_keys = { SortType::NAME, SortType::DATE }; + bool m_can_interact = true; QDir m_dir; -- cgit From 256f8094f5fed85ff9136e8d0b9c9677d7b9e9db Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 10 Aug 2022 14:46:28 -0300 Subject: refactor: make Resource Pack model inherit from ResourceFolderModel Signed-off-by: flow --- launcher/minecraft/mod/ResourcePackFolderModel.cpp | 21 +-------------------- launcher/minecraft/mod/ResourcePackFolderModel.h | 7 ++----- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 276804ed..64a04469 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,24 +35,5 @@ #include "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { -} - -QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role == Qt::ToolTipRole) { - switch (section) { - case ActiveColumn: - return tr("Is the resource pack enabled?"); - case NameColumn: - return tr("The name of the resource pack."); - case VersionColumn: - return tr("The version of the resource pack."); - case DateColumn: - return tr("The date and time this resource pack was last changed (or added)."); - default: - return QVariant(); - } - } - - return ModFolderModel::headerData(section, orientation, role); +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(dir) { } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 0cd6214b..d2a5bf18 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -1,13 +1,10 @@ #pragma once -#include "ModFolderModel.h" +#include "ResourceFolderModel.h" -class ResourcePackFolderModel : public ModFolderModel +class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT - public: explicit ResourcePackFolderModel(const QString &dir); - - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; }; -- cgit From 97a74d5c1f00a11d331a41b16690f7202fe102a3 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 10 Aug 2022 14:48:34 -0300 Subject: refactor: adapt rest of the codebase to the new resource model In order to access the ModFolderModel from the ModFolderPage, i created a new m_model for the correct type, shadowing the m_model of type ResourceFolderModel. This creates two shared_ptr references to the same object, but since they will have the same lifetime, it doesn't generate a memory leak. Signed-off-by: flow --- launcher/CMakeLists.txt | 7 +- launcher/InstancePageProvider.h | 6 +- launcher/minecraft/MinecraftInstance.cpp | 13 +- launcher/minecraft/MinecraftInstance.h | 16 +- launcher/minecraft/mod/Mod.h | 3 +- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- launcher/minecraft/mod/Resource.h | 2 + launcher/minecraft/mod/ResourceFolderModel.h | 3 + launcher/minecraft/mod/ResourcePack.h | 13 ++ launcher/minecraft/mod/ResourcePackFolderModel.cpp | 3 +- launcher/minecraft/mod/ResourcePackFolderModel.h | 4 + launcher/minecraft/mod/ShaderPackFolderModel.h | 10 ++ launcher/minecraft/mod/TexturePackFolderModel.cpp | 22 +-- launcher/minecraft/mod/TexturePackFolderModel.h | 6 +- .../ui/pages/instance/ExternalResourcesPage.cpp | 142 ++--------------- launcher/ui/pages/instance/ExternalResourcesPage.h | 17 +- .../ui/pages/instance/ExternalResourcesPage.ui | 6 +- launcher/ui/pages/instance/ModFolderPage.cpp | 42 ++++- launcher/ui/pages/instance/ModFolderPage.h | 11 ++ launcher/ui/pages/instance/ResourcePackPage.h | 6 +- launcher/ui/pages/instance/ShaderPackPage.h | 6 +- launcher/ui/pages/instance/TexturePackPage.h | 6 +- launcher/ui/pages/instance/VersionPage.cpp | 6 +- launcher/ui/pages/instance/VersionPage.ui | 6 +- launcher/ui/widgets/InfoFrame.cpp | 172 +++++++++++++++++++++ launcher/ui/widgets/InfoFrame.h | 54 +++++++ launcher/ui/widgets/InfoFrame.ui | 92 +++++++++++ launcher/ui/widgets/MCModInfoFrame.cpp | 168 -------------------- launcher/ui/widgets/MCModInfoFrame.h | 52 ------- launcher/ui/widgets/MCModInfoFrame.ui | 92 ----------- 30 files changed, 480 insertions(+), 508 deletions(-) create mode 100644 launcher/minecraft/mod/ResourcePack.h create mode 100644 launcher/minecraft/mod/ShaderPackFolderModel.h create mode 100644 launcher/ui/widgets/InfoFrame.cpp create mode 100644 launcher/ui/widgets/InfoFrame.h create mode 100644 launcher/ui/widgets/InfoFrame.ui delete mode 100644 launcher/ui/widgets/MCModInfoFrame.cpp delete mode 100644 launcher/ui/widgets/MCModInfoFrame.h delete mode 100644 launcher/ui/widgets/MCModInfoFrame.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index d1e0befd..badd0eaa 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -326,6 +326,7 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/ShaderPackFolderModel.h minecraft/mod/tasks/BasicFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -884,8 +885,8 @@ SET(LAUNCHER_SOURCES ui/widgets/LineSeparator.h ui/widgets/LogView.cpp ui/widgets/LogView.h - ui/widgets/MCModInfoFrame.cpp - ui/widgets/MCModInfoFrame.h + ui/widgets/InfoFrame.cpp + ui/widgets/InfoFrame.h ui/widgets/ModFilterWidget.cpp ui/widgets/ModFilterWidget.h ui/widgets/ModListView.cpp @@ -947,7 +948,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui - ui/widgets/MCModInfoFrame.ui + ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/ProfileSetupDialog.ui diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 78fb7016..bf29377d 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -37,9 +37,9 @@ public: modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); values.append(modsPage); values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); - values.append(new ResourcePackPage(onesix.get())); - values.append(new TexturePackPage(onesix.get())); - values.append(new ShaderPackPage(onesix.get())); + values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); + values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); + values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix.get(), onesix->worldList())); values.append(new ServersPage(onesix)); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index b42aeda3..94b4776e 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -76,6 +76,7 @@ #include "mod/ModFolderModel.h" #include "mod/ResourcePackFolderModel.h" +#include "mod/ShaderPackFolderModel.h" #include "mod/TexturePackFolderModel.h" #include "WorldList.h" @@ -1092,18 +1093,18 @@ std::shared_ptr MinecraftInstance::coreModList() const return m_core_mod_list; } -std::shared_ptr MinecraftInstance::resourcePackList() const +std::shared_ptr MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); - m_resource_pack_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); + m_resource_pack_list->enableInteraction(!isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction); } return m_resource_pack_list; } -std::shared_ptr MinecraftInstance::texturePackList() const +std::shared_ptr MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { @@ -1114,11 +1115,11 @@ std::shared_ptr MinecraftInstance::texturePackList() const return m_texture_pack_list; } -std::shared_ptr MinecraftInstance::shaderPackList() const +std::shared_ptr MinecraftInstance::shaderPackList() const { if (!m_shader_pack_list) { - m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir())); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir())); m_shader_pack_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction); } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 7a75f452..d62ac655 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -7,6 +7,10 @@ #include "minecraft/launch/MinecraftServerTarget.h" class ModFolderModel; +class ResourceFolderModel; +class ResourcePackFolderModel; +class ShaderPackFolderModel; +class TexturePackFolderModel; class WorldList; class GameOptions; class LaunchStep; @@ -72,9 +76,9 @@ public: ////// Mod Lists ////// std::shared_ptr loaderModList() const; std::shared_ptr coreModList() const; - std::shared_ptr resourcePackList() const; - std::shared_ptr texturePackList() const; - std::shared_ptr shaderPackList() const; + std::shared_ptr resourcePackList() const; + std::shared_ptr texturePackList() const; + std::shared_ptr shaderPackList() const; std::shared_ptr worldList() const; std::shared_ptr gameOptionsModel() const; @@ -125,9 +129,9 @@ protected: // data std::shared_ptr m_components; mutable std::shared_ptr m_loader_mod_list; mutable std::shared_ptr m_core_mod_list; - mutable std::shared_ptr m_resource_pack_list; - mutable std::shared_ptr m_shader_pack_list; - mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_resource_pack_list; + mutable std::shared_ptr m_shader_pack_list; + mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_world_list; mutable std::shared_ptr m_game_options; }; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 25ac88c9..0835e3b1 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -50,7 +50,8 @@ public: Mod() = default; Mod(const QFileInfo &file); - explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); + Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); + Mod(QString file_path) : Mod(QFileInfo(file_path)) {} auto enabled() const -> bool { return m_enabled; } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 9b8c58bc..c316e710 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -49,7 +49,7 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(dir), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 68663ab0..bec7490f 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -34,6 +34,8 @@ class Resource : public QObject { Resource(QObject* parent = nullptr); Resource(QFileInfo file_info); + Resource(QString file_path) : Resource(QFileInfo(file_path)) {} + ~Resource() override = default; void setFile(QFileInfo file_info); diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 2ccf14f0..986d9885 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -261,6 +261,9 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet std::sort(removed_rows.begin(), removed_rows.end()); + for (int i = 0; i < removed_rows.size(); i++) + removed_rows[i] -= i; + for (auto& removed_index : removed_rows) { beginRemoveRows(QModelIndex(), removed_index, removed_index); diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h new file mode 100644 index 00000000..c2cc8690 --- /dev/null +++ b/launcher/minecraft/mod/ResourcePack.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Resource.h" + +class ResourcePack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + ResourcePack(QObject* parent = nullptr) : Resource(parent) {} + ResourcePack(QFileInfo file_info) : Resource(file_info) {} + +}; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 64a04469..e92be894 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,5 +35,4 @@ #include "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(dir) { -} +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index d2a5bf18..1fe82867 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -2,9 +2,13 @@ #include "ResourceFolderModel.h" +#include "ResourcePack.h" + class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: explicit ResourcePackFolderModel(const QString &dir); + + RESOURCE_HELPERS(ResourcePack) }; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h new file mode 100644 index 00000000..a3aa958f --- /dev/null +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -0,0 +1,10 @@ +#pragma once + +#include "ResourceFolderModel.h" + +class ShaderPackFolderModel : public ResourceFolderModel { + Q_OBJECT + + public: + explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {} +}; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index e3a22219..2c7c945b 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -35,24 +35,4 @@ #include "TexturePackFolderModel.h" -TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { -} - -QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role == Qt::ToolTipRole) { - switch (section) { - case ActiveColumn: - return tr("Is the texture pack enabled?"); - case NameColumn: - return tr("The name of the texture pack."); - case VersionColumn: - return tr("The version of the texture pack."); - case DateColumn: - return tr("The date and time this texture pack was last changed (or added)."); - default: - return QVariant(); - } - } - - return ModFolderModel::headerData(section, orientation, role); -} +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index a59d5119..69e98661 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -1,13 +1,11 @@ #pragma once -#include "ModFolderModel.h" +#include "ResourceFolderModel.h" -class TexturePackFolderModel : public ModFolderModel +class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: explicit TexturePackFolderModel(const QString &dir); - - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index da7c4af0..0a3687e5 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -3,100 +3,13 @@ #include "DesktopServices.h" #include "Version.h" -#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "ui/GuiUtil.h" #include #include -namespace { -// FIXME: wasteful -void RemoveThePrefix(QString& string) -{ - QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); - string.remove(regex); - string = string.trimmed(); -} -} // namespace - -class SortProxy : public QSortFilterProxyModel { - public: - explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} - - protected: - bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override - { - ModFolderModel* model = qobject_cast(sourceModel()); - if (!model) - return false; - - const auto& mod = model->at(source_row); - - if (filterRegularExpression().match(mod->name()).hasMatch()) - return true; - if (filterRegularExpression().match(mod->description()).hasMatch()) - return true; - - for (auto& author : mod->authors()) { - if (filterRegularExpression().match(author).hasMatch()) { - return true; - } - } - - return false; - } - - bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override - { - ModFolderModel* model = qobject_cast(sourceModel()); - if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { - return QSortFilterProxyModel::lessThan(source_left, source_right); - } - - // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and - // proceed. - - auto column = (ModFolderModel::Columns) source_left.column(); - bool invert = false; - switch (column) { - // GH-2550 - sort by enabled/disabled - case ModFolderModel::ActiveColumn: { - auto dataL = source_left.data(Qt::CheckStateRole).toBool(); - auto dataR = source_right.data(Qt::CheckStateRole).toBool(); - if (dataL != dataR) - return dataL > dataR; - - // fallthrough - invert = sortOrder() == Qt::DescendingOrder; - } - // GH-2722 - sort mod names in a way that discards "The" prefixes - case ModFolderModel::NameColumn: { - auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString(); - RemoveThePrefix(dataL); - auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString(); - RemoveThePrefix(dataR); - - auto less = dataL.compare(dataR, sortCaseSensitivity()); - if (less != 0) - return invert ? (less > 0) : (less < 0); - - // fallthrough - invert = sortOrder() == Qt::DescendingOrder; - } - // GH-2762 - sort versions by parsing them as versions - case ModFolderModel::VersionColumn: { - auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString()); - auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString()); - return invert ? (dataL > dataR) : (dataL < dataR); - } - default: { - return QSortFilterProxyModel::lessThan(source_left, source_right); - } - } - } -}; - -ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) +ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) { ui->setupUi(this); @@ -105,7 +18,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); - m_filterModel = new SortProxy(this); + m_filterModel = model->createFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); @@ -127,7 +40,6 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); connect(ui->treeView, &ModListView::customContextMenuRequested, this, &ExternalResourcesPage::ShowContextMenu); - connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); @@ -137,19 +49,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared ExternalResourcesPage::~ExternalResourcesPage() { - m_model->stopWatching(); delete ui; } -void ExternalResourcesPage::itemActivated(const QModelIndex&) -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle); -} - QMenu* ExternalResourcesPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); @@ -241,7 +143,7 @@ void ExternalResourcesPage::addItem() if (!list.isEmpty()) { for (auto filename : list) { - m_model->installMod(filename); + m_model->installResource(filename); } } } @@ -252,25 +154,7 @@ void ExternalResourcesPage::removeItem() return; auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->deleteMods(selection.indexes()); -} - -void ExternalResourcesPage::enableItem() -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Enable); -} - -void ExternalResourcesPage::disableItem() -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Disable); + m_model->deleteResources(selection.indexes()); } void ExternalResourcesPage::viewConfigs() @@ -283,15 +167,23 @@ void ExternalResourcesPage::viewFolder() DesktopServices::openDirectory(m_model->dir().absolutePath(), true); } -void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) +bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) { if (!current.isValid()) { ui->frame->clear(); - return; + return false; } + return onSelectionChanged(current, previous); +} + +bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +{ auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - Mod& m = *m_model->operator[](row); - ui->frame->updateWithMod(m); + Resource const& resource = m_model->at(row); + ui->frame->updateWithResource(resource); + + return true; } + diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index ff294678..280f1542 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -7,7 +7,7 @@ #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" -class ModFolderModel; +class ResourceFolderModel; namespace Ui { class ExternalResourcesPage; @@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { Q_OBJECT public: - // FIXME: Switch to different model (or change the name of this one) - explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); virtual ~ExternalResourcesPage(); virtual QString displayName() const override = 0; @@ -41,18 +40,20 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; public slots: - void current(const QModelIndex& current, const QModelIndex& previous); + bool current(const QModelIndex& current, const QModelIndex& previous); + + virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous); protected slots: - void itemActivated(const QModelIndex& index); + virtual void itemActivated(const QModelIndex& index) {}; void filterTextChanged(const QString& newContents); virtual void runningStateChanged(bool running); virtual void addItem(); virtual void removeItem(); - virtual void enableItem(); - virtual void disableItem(); + virtual void enableItem() {}; + virtual void disableItem() {}; virtual void viewFolder(); virtual void viewConfigs(); @@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { BaseInstance* m_instance = nullptr; Ui::ExternalResourcesPage* ui = nullptr; - std::shared_ptr m_model; + std::shared_ptr m_model; QSortFilterProxyModel* m_filterModel = nullptr; QString m_fileSelectionFilter; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index a13666b2..76f8ec18 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -43,7 +43,7 @@ - + 0 @@ -166,9 +166,9 @@
ui/widgets/ModListView.h
- MCModInfoFrame + InfoFrame QFrame -
ui/widgets/MCModInfoFrame.h
+
ui/widgets/InfoFrame.h
1
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 45678db1..63897fb0 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -65,7 +65,7 @@ #include "ui/dialogs/ProgressDialog.h" ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ExternalResourcesPage(inst, mods, parent) + : ExternalResourcesPage(inst, mods, parent), m_model(mods) { // This is structured like that so that these changes // do not affect the Resource pack and Shader pack tabs @@ -110,6 +110,8 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); } + + connect(ui->treeView, &ModListView::activated, this, &ModFolderPage::itemActivated); } void ModFolderPage::runningStateChanged(bool running) @@ -124,6 +126,44 @@ bool ModFolderPage::shouldDisplay() const return true; } +void ModFolderPage::itemActivated(const QModelIndex&) +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle); +} + +void ModFolderPage::enableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Enable); +} + +void ModFolderPage::disableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Disable); +} + +bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + Mod const* m = m_model->at(row); + if (m) + ui->frame->updateWithMod(*m); + + return true; +} + void ModFolderPage::installMods() { if (!m_controlsEnabled) diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 7e305951..5da353f0 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -55,9 +55,20 @@ class ModFolderPage : public ExternalResourcesPage { virtual bool shouldDisplay() const override; void runningStateChanged(bool running) override; + public slots: + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + + void itemActivated(const QModelIndex& index) override; + + void enableItem() override; + void disableItem() override; + private slots: void installMods(); void updateMods(); + + protected: + std::shared_ptr m_model; }; class CoreModFolderPage : public ModFolderPage { diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index a6c9fdd3..2eefc3d3 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -38,12 +38,14 @@ #include "ExternalResourcesPage.h" #include "ui_ExternalResourcesPage.h" +#include "minecraft/mod/ResourcePackFolderModel.h" + class ResourcePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ExternalResourcesPage(instance, instance->resourcePackList(), parent) + explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr model, QWidget *parent = 0) + : ExternalResourcesPage(instance, model, parent) { ui->actionViewConfigs->setVisible(false); } diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 2cc056c8..7f7ff8c1 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -38,12 +38,14 @@ #include "ExternalResourcesPage.h" #include "ui_ExternalResourcesPage.h" +#include "minecraft/mod/ShaderPackFolderModel.h" + class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ExternalResourcesPage(instance, instance->shaderPackList(), parent) + explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr model, QWidget *parent = 0) + : ExternalResourcesPage(instance, model, parent) { ui->actionViewConfigs->setVisible(false); } diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index f550a5bc..fa219eda 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -38,12 +38,14 @@ #include "ExternalResourcesPage.h" #include "ui_ExternalResourcesPage.h" +#include "minecraft/mod/TexturePackFolderModel.h" + class TexturePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ExternalResourcesPage(instance, instance->texturePackList(), parent) + explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr model, QWidget *parent = 0) + : ExternalResourcesPage(instance, model, parent) { ui->actionViewConfigs->setVisible(false); } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 468ff35c..a021c633 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex & switch(severity) { case ProblemSeverity::Warning: - ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName())); + ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName())); break; case ProblemSeverity::Error: - ui->frame->setModText(tr("%1 has issues!").arg(patch->getName())); + ui->frame->setName(tr("%1 has issues!").arg(patch->getName())); break; default: case ProblemSeverity::None: @@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex & problemOut += problem.m_description; problemOut += "\n"; } - ui->frame->setModDescription(problemOut); + ui->frame->setDescription(problemOut); } void VersionPage::updateRunningStatus(bool running) diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 489f7218..fcba5598 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -64,7 +64,7 @@
- + 0 @@ -278,9 +278,9 @@
ui/widgets/ModListView.h
- MCModInfoFrame + InfoFrame QFrame -
ui/widgets/MCModInfoFrame.h
+
ui/widgets/InfoFrame.h
1
diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp new file mode 100644 index 00000000..821e61a7 --- /dev/null +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -0,0 +1,172 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "InfoFrame.h" +#include "ui_InfoFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +InfoFrame::InfoFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::InfoFrame) +{ + ui->setupUi(this); + ui->descriptionLabel->setHidden(true); + ui->nameLabel->setHidden(true); + updateHiddenState(); +} + +InfoFrame::~InfoFrame() +{ + delete ui; +} + +void InfoFrame::updateWithMod(Mod const& m) +{ + if (m.type() == ResourceType::FOLDER) + { + clear(); + return; + } + + QString text = ""; + QString name = ""; + if (m.name().isEmpty()) + name = m.internal_id(); + else + name = m.name(); + + if (m.homeurl().isEmpty()) + text = name; + else + text = "" + name + ""; + if (!m.authors().isEmpty()) + text += " by " + m.authors().join(", "); + + setName(text); + + if (m.description().isEmpty()) + { + setDescription(QString()); + } + else + { + setDescription(m.description()); + } +} + +void InfoFrame::updateWithResource(const Resource& resource) +{ + setName(resource.name()); +} + +void InfoFrame::clear() +{ + setName(); + setDescription(); +} + +void InfoFrame::updateHiddenState() +{ + if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void InfoFrame::setName(QString text) +{ + if(text.isEmpty()) + { + ui->nameLabel->setHidden(true); + } + else + { + ui->nameLabel->setText(text); + ui->nameLabel->setHidden(false); + } + updateHiddenState(); +} + +void InfoFrame::setDescription(QString text) +{ + if(text.isEmpty()) + { + ui->descriptionLabel->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->descriptionLabel->setHidden(false); + updateHiddenState(); + } + ui->descriptionLabel->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->descriptionLabel->setOpenExternalLinks(false); + ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); + m_description = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + } + else + { + ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText); + labeltext.append(finaltext); + } + ui->descriptionLabel->setText(labeltext); +} + +void InfoFrame::descriptionEllipsisHandler(QString link) +{ + if(!m_current_box) + { + m_current_box = CustomMessageBox::selectable(this, "", m_description); + connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); + m_current_box->show(); + } + else + { + m_current_box->setText(m_description); + } +} + +void InfoFrame::boxClosed(int result) +{ + m_current_box = nullptr; +} diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h new file mode 100644 index 00000000..d69dc232 --- /dev/null +++ b/launcher/ui/widgets/InfoFrame.h @@ -0,0 +1,54 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ResourcePack.h" + +namespace Ui +{ +class InfoFrame; +} + +class InfoFrame : public QFrame { + Q_OBJECT + + public: + InfoFrame(QWidget* parent = nullptr); + ~InfoFrame() override; + + void setName(QString text = {}); + void setDescription(QString text = {}); + + void clear(); + + void updateWithMod(Mod const& m); + void updateWithResource(Resource const& resource); + + public slots: + void descriptionEllipsisHandler(QString link); + void boxClosed(int result); + + private: + void updateHiddenState(); + + private: + Ui::InfoFrame* ui; + QString m_description; + class QMessageBox* m_current_box = nullptr; +}; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui new file mode 100644 index 00000000..0d3772d7 --- /dev/null +++ b/launcher/ui/widgets/InfoFrame.ui @@ -0,0 +1,92 @@ + + + InfoFrame + + + + 0 + 0 + 527 + 113 + + + + + 0 + 0 + + + + + 16777215 + 120 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp deleted file mode 100644 index 22475abc..00000000 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ /dev/null @@ -1,168 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include "MCModInfoFrame.h" -#include "ui_MCModInfoFrame.h" - -#include "ui/dialogs/CustomMessageBox.h" - -void MCModInfoFrame::updateWithMod(Mod &m) -{ - if (m.type() == ResourceType::FOLDER) - { - clear(); - return; - } - - QString text = ""; - QString name = ""; - if (m.name().isEmpty()) - name = m.internal_id(); - else - name = m.name(); - - if (m.homeurl().isEmpty()) - text = name; - else - text = "" + name + ""; - if (!m.authors().isEmpty()) - text += " by " + m.authors().join(", "); - - setModText(text); - - if (m.description().isEmpty()) - { - setModDescription(QString()); - } - else - { - setModDescription(m.description()); - } -} - -void MCModInfoFrame::clear() -{ - setModText(QString()); - setModDescription(QString()); -} - -MCModInfoFrame::MCModInfoFrame(QWidget *parent) : - QFrame(parent), - ui(new Ui::MCModInfoFrame) -{ - ui->setupUi(this); - ui->label_ModDescription->setHidden(true); - ui->label_ModText->setHidden(true); - updateHiddenState(); -} - -MCModInfoFrame::~MCModInfoFrame() -{ - delete ui; -} - -void MCModInfoFrame::updateHiddenState() -{ - if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden()) - { - setHidden(true); - } - else - { - setHidden(false); - } -} - -void MCModInfoFrame::setModText(QString text) -{ - if(text.isEmpty()) - { - ui->label_ModText->setHidden(true); - } - else - { - ui->label_ModText->setText(text); - ui->label_ModText->setHidden(false); - } - updateHiddenState(); -} - -void MCModInfoFrame::setModDescription(QString text) -{ - if(text.isEmpty()) - { - ui->label_ModDescription->setHidden(true); - updateHiddenState(); - return; - } - else - { - ui->label_ModDescription->setHidden(false); - updateHiddenState(); - } - ui->label_ModDescription->setToolTip(""); - QString intermediatetext = text.trimmed(); - bool prev(false); - QChar rem('\n'); - QString finaltext; - finaltext.reserve(intermediatetext.size()); - foreach(const QChar& c, intermediatetext) - { - if(c == rem && prev){ - continue; - } - prev = c == rem; - finaltext += c; - } - QString labeltext; - labeltext.reserve(300); - if(finaltext.length() > 290) - { - ui->label_ModDescription->setOpenExternalLinks(false); - ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText); - desc = text; - // This allows injecting HTML here. - labeltext.append("" + finaltext.left(287) + "..."); - QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler); - } - else - { - ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText); - labeltext.append(finaltext); - } - ui->label_ModDescription->setText(labeltext); -} - -void MCModInfoFrame::modDescEllipsisHandler(const QString &link) -{ - if(!currentBox) - { - currentBox = CustomMessageBox::selectable(this, QString(), desc); - connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed); - currentBox->show(); - } - else - { - currentBox->setText(desc); - } -} - -void MCModInfoFrame::boxClosed(int result) -{ - currentBox = nullptr; -} diff --git a/launcher/ui/widgets/MCModInfoFrame.h b/launcher/ui/widgets/MCModInfoFrame.h deleted file mode 100644 index 0b7ef537..00000000 --- a/launcher/ui/widgets/MCModInfoFrame.h +++ /dev/null @@ -1,52 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include "minecraft/mod/Mod.h" - -namespace Ui -{ -class MCModInfoFrame; -} - -class MCModInfoFrame : public QFrame -{ - Q_OBJECT - -public: - explicit MCModInfoFrame(QWidget *parent = 0); - ~MCModInfoFrame(); - - void setModText(QString text); - void setModDescription(QString text); - - void updateWithMod(Mod &m); - void clear(); - -public slots: - void modDescEllipsisHandler(const QString& link ); - void boxClosed(int result); - -private: - void updateHiddenState(); - -private: - Ui::MCModInfoFrame *ui; - QString desc; - class QMessageBox * currentBox = nullptr; -}; - diff --git a/launcher/ui/widgets/MCModInfoFrame.ui b/launcher/ui/widgets/MCModInfoFrame.ui deleted file mode 100644 index 5ef33379..00000000 --- a/launcher/ui/widgets/MCModInfoFrame.ui +++ /dev/null @@ -1,92 +0,0 @@ - - - MCModInfoFrame - - - - 0 - 0 - 527 - 113 - - - - - 0 - 0 - - - - - 16777215 - 120 - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - -- cgit From 92aa24ae345c7b50028ea672c0d175d87e906c59 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 11 Aug 2022 18:24:26 -0300 Subject: fix: don't give shared pointer to obj. external to the model It causes some weird problems and adds refcounting overhead. Signed-off-by: flow --- launcher/minecraft/mod/Mod.h | 1 + launcher/minecraft/mod/ModFolderModel.cpp | 11 ++-- launcher/minecraft/mod/ModFolderModel.h | 4 +- launcher/minecraft/mod/Resource.h | 2 + launcher/minecraft/mod/ResourceFolderModel.cpp | 6 +- launcher/minecraft/mod/ResourceFolderModel.h | 71 +++++++++++----------- launcher/minecraft/mod/tasks/BasicFolderLoadTask.h | 2 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 2 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 9 ++- launcher/ui/dialogs/ModUpdateDialog.h | 4 +- 10 files changed, 58 insertions(+), 54 deletions(-) diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 0835e3b1..49326c83 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -47,6 +47,7 @@ class Mod : public Resource Q_OBJECT public: using Ptr = shared_qobject_ptr; + using WeakPtr = QPointer; Mod() = default; Mod(const QFileInfo &file); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index c316e710..8bdab16e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -226,9 +226,9 @@ bool ModFolderModel::stopWatching() return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList { - QList selected_resources; + QList selected_resources; for (auto i : indexes) { if(i.column() != 0) continue; @@ -238,12 +238,13 @@ auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList return selected_resources; } -auto ModFolderModel::allMods() -> QList +auto ModFolderModel::allMods() -> QList { - QList mods; + QList mods; - for (auto res : m_resources) + for (auto& res : m_resources) { mods.append(static_cast(res.get())); + } return mods; } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index b1f30710..7fe830c2 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -101,8 +101,8 @@ public: QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } - auto selectedMods(QModelIndexList& indexes) -> QList; - auto allMods() -> QList; + auto selectedMods(QModelIndexList& indexes) -> QList; + auto allMods() -> QList; RESOURCE_HELPERS(Mod) diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index bec7490f..e76bc49e 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "QObjectPtr.h" @@ -31,6 +32,7 @@ class Resource : public QObject { Q_DISABLE_COPY(Resource) public: using Ptr = shared_qobject_ptr; + using WeakPtr = QPointer; Resource(QObject* parent = nullptr); Resource(QFileInfo file_info); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 982915e2..c27a5e2d 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -135,7 +135,7 @@ bool ResourceFolderModel::installResource(QString original_path) bool ResourceFolderModel::uninstallResource(QString file_name) { - for (auto resource : m_resources) { + for (auto& resource : m_resources) { if (resource->fileinfo().fileName() == file_name) return resource->destroy(); } @@ -155,7 +155,7 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) continue; } - auto resource = m_resources.at(i.row()); + auto& resource = m_resources.at(i.row()); resource->destroy(); } return true; @@ -183,7 +183,7 @@ bool ResourceFolderModel::update() return true; } -void ResourceFolderModel::resolveResource(Resource::Ptr res) +void ResourceFolderModel::resolveResource(Resource::WeakPtr res) { if (!res->shouldResolve()) { return; diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 986d9885..59d2388a 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -15,10 +15,10 @@ class QSortFilterProxyModel; /** A basic model for external resources. * - * To implement one such model, you need to implement, at the very minimum: - * - columnCount: The number of columns in your model. - * - data: How the model data is displayed and accessed. - * - headerData: Display properties of the header. + * This model manages a list of resources. As such, external users of such resources do not own them, + * and the resource's lifetime is contingent on the model's lifetime. + * + * TODO: Make the resources unique pointers accessible through weak pointers. */ class ResourceFolderModel : public QAbstractListModel { Q_OBJECT @@ -62,7 +62,7 @@ class ResourceFolderModel : public QAbstractListModel { virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ - virtual void resolveResource(Resource::Ptr res); + virtual void resolveResource(Resource::WeakPtr res); [[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] bool empty() const { return size() == 0; } @@ -151,7 +151,7 @@ class ResourceFolderModel : public QAbstractListModel { * * This usually calls static_cast on the specific Task type returned by createUpdateTask, * so care must be taken in such cases. - * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro dissalows that). + * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that). */ virtual void onUpdateSucceeded(); virtual void onUpdateFailed() {} @@ -189,33 +189,34 @@ class ResourceFolderModel : public QAbstractListModel { }; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ -#define RESOURCE_HELPERS(T) \ - [[nodiscard]] T* operator[](size_t index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] T* at(size_t index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] const T* at(size_t index) const \ - { \ - return static_cast(m_resources.at(index).get()); \ - } \ - [[nodiscard]] T* first() \ - { \ - return static_cast(m_resources.first().get()); \ - } \ - [[nodiscard]] T* last() \ - { \ - return static_cast(m_resources.last().get()); \ - } \ - [[nodiscard]] T* find(QString id) \ - { \ - auto iter = std::find_if(m_resources.begin(), m_resources.end(), [&](Resource::Ptr r) { return r->internal_id() == id; }); \ - if (iter == m_resources.end()) \ - return nullptr; \ - return static_cast((*iter).get()); \ +#define RESOURCE_HELPERS(T) \ + [[nodiscard]] T* operator[](size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] T* at(size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] const T* at(size_t index) const \ + { \ + return static_cast(m_resources.at(index).get()); \ + } \ + [[nodiscard]] T* first() \ + { \ + return static_cast(m_resources.first().get()); \ + } \ + [[nodiscard]] T* last() \ + { \ + return static_cast(m_resources.last().get()); \ + } \ + [[nodiscard]] T* find(QString id) \ + { \ + auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \ + [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \ + if (iter == m_resources.constEnd()) \ + return nullptr; \ + return static_cast((*iter).get()); \ } /* Template definition to avoid some code duplication */ @@ -231,7 +232,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet auto row = m_resources_index[kept]; auto new_resource = new_resources[kept]; - auto current_resource = m_resources[row]; + auto const& current_resource = m_resources[row]; if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { // no significant change, ignore... @@ -301,7 +302,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet { m_resources_index.clear(); int idx = 0; - for (auto mod : m_resources) { + for (auto const& mod : m_resources) { m_resources_index[mod->internal_id()] = idx; idx++; } diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index 0fd5c292..2944c747 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -17,7 +17,7 @@ class BasicFolderLoadTask : public Task Q_OBJECT public: struct Result { - QMap resources; + QMap resources; }; using ResultPtr = std::shared_ptr; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index e8180c11..44cb1d5f 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -52,7 +52,7 @@ void ModFolderLoadTask::executeTask() // Read JAR files that don't have metadata m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { - Mod::Ptr mod(new Mod(entry)); + Mod* mod(new Mod(entry)); if (mod->enabled()) { if (m_result->mods.contains(mod->internal_id())) { diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index d73c8ebb..a91f6e5c 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) ModUpdateDialog::ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mods, - QList& search_for) + QList& search_for) : ReviewMessageBox(parent, tr("Confirm mods to update"), "") , m_parent(parent) , m_mod_model(mods) @@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool }; for (auto candidate : m_candidates) { - auto* candidate_ptr = candidate.get(); if (candidate->status() != ModStatus::NoMetadata) { - onMetadataEnsured(candidate_ptr); + onMetadataEnsured(candidate); continue; } @@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool continue; if (confirm_rest) { - addToTmp(candidate_ptr, provider_rest); + addToTmp(candidate, provider_rest); should_try_others.insert(candidate->internal_id(), try_others_rest); continue; } @@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool should_try_others.insert(candidate->internal_id(), response.try_others); if (confirmed) - addToTmp(candidate_ptr, response.chosen); + addToTmp(candidate, response.chosen); } if (!modrinth_tmp.empty()) { diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index 76aaab36..bd486f0d 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox { explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mod_model, - QList& search_for); + QList& search_for); void checkCandidates(); @@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox { const std::shared_ptr m_mod_model; - QList& m_candidates; + QList& m_candidates; QList m_modrinth_to_update; QList m_flame_to_update; -- cgit From 0c9d03f5dffc37f3eda500fd520907a142ac061f Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 7 Aug 2022 08:34:26 -0300 Subject: fix(tests): add timeout on ModFolderModel's tests If the update never ends, the signal is not emitted and we become stuck in the event loop forever. So a very lenient timer is added to prevent that. Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel_test.cpp | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index b4d37ce5..1b50ebd6 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -35,6 +35,7 @@ #include #include +#include #include "FileSystem.h" #include "minecraft/mod/ModFolderModel.h" @@ -65,11 +66,25 @@ slots: { QString folder = source; QTemporaryDir tempDir; + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); + + QTimer expire_timer; + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); + expire_timer.setSingleShot(true); + expire_timer.start(4000); + m.installMod(folder); + loop.exec(); + + QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); + expire_timer.stop(); + verify(tempDir.path()); } @@ -79,9 +94,21 @@ slots: QTemporaryDir tempDir; QEventLoop loop; ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); + + QTimer expire_timer; + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); + expire_timer.setSingleShot(true); + expire_timer.start(4000); + m.installMod(folder); + loop.exec(); + + QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); + expire_timer.stop(); + verify(tempDir.path()); } } -- cgit From e7cf9932a9695417d40d895ac6174186f074f053 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 12 Aug 2022 17:06:20 -0300 Subject: refactor: simplify Mod structure No need to keep track of pointers left and right. A single one already gives enough headaches! Signed-off-by: flow --- launcher/CMakeLists.txt | 4 +- launcher/minecraft/mod/Mod.cpp | 50 +++------ launcher/minecraft/mod/Mod.h | 14 +-- launcher/minecraft/mod/ModDetails.h | 33 ++++-- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 114 ++++++++++----------- launcher/minecraft/mod/tasks/LocalModParseTask.h | 2 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 4 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 2 +- 9 files changed, 107 insertions(+), 118 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index badd0eaa..4f5fa2fc 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -381,8 +381,8 @@ ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER # FIXME: shares data with FileSystem test # TODO: needs testdata -ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME ModFolderModel) +ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ResourceFolderModel) ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ed91d999..5e186471 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -44,13 +44,7 @@ #include "MetadataHandler.h" #include "Version.h" -namespace { - -ModDetails invalidDetails; - -} - -Mod::Mod(const QFileInfo& file) : Resource(file) +Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); } @@ -59,7 +53,7 @@ Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) : Mod(mods_dir.absoluteFilePath(metadata.filename)) { m_name = metadata.name; - m_temp_metadata = std::make_shared(std::move(metadata)); + m_local_details.metadata = std::make_shared(std::move(metadata)); } auto Mod::enable(bool value) -> bool @@ -95,22 +89,14 @@ auto Mod::enable(bool value) -> bool void Mod::setStatus(ModStatus status) { - if (m_localDetails) { - m_localDetails->status = status; - } else { - m_temp_status = status; - } + m_local_details.status = status; } -void Mod::setMetadata(const Metadata::ModStruct& metadata) +void Mod::setMetadata(std::shared_ptr&& metadata) { if (status() == ModStatus::NoMetadata) setStatus(ModStatus::Installed); - if (m_localDetails) { - m_localDetails->metadata = std::make_shared(std::move(metadata)); - } else { - m_temp_metadata = std::make_shared(std::move(metadata)); - } + m_local_details.metadata = metadata; } std::pair Mod::compare(const Resource& other, SortType type) const @@ -176,7 +162,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool auto Mod::details() const -> const ModDetails& { - return m_localDetails ? *m_localDetails : invalidDetails; + return m_local_details; } auto Mod::name() const -> QString @@ -213,35 +199,29 @@ auto Mod::authors() const -> QStringList auto Mod::status() const -> ModStatus { - if (!m_localDetails) - return m_temp_status; return details().status; } auto Mod::metadata() -> std::shared_ptr { - if (m_localDetails) - return m_localDetails->metadata; - return m_temp_metadata; + return m_local_details.metadata; } auto Mod::metadata() const -> const std::shared_ptr { - if (m_localDetails) - return m_localDetails->metadata; - return m_temp_metadata; + return m_local_details.metadata; } -void Mod::finishResolvingWithDetails(std::shared_ptr details) +void Mod::finishResolvingWithDetails(ModDetails&& details) { m_is_resolving = false; m_is_resolved = true; - m_localDetails = details; - setStatus(m_temp_status); + std::shared_ptr metadata = details.metadata; + if (details.status == ModStatus::Unknown) + details.status = m_local_details.status; - if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { - setMetadata(*m_temp_metadata); - m_temp_metadata.reset(); - } + m_local_details = std::move(details); + if (metadata) + setMetadata(std::move(metadata)); } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 49326c83..b9b57058 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -68,7 +68,8 @@ public: auto metadata() const -> const std::shared_ptr; void setStatus(ModStatus status); - void setMetadata(const Metadata::ModStruct& metadata); + void setMetadata(std::shared_ptr&& metadata); + void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } auto enable(bool value) -> bool; @@ -78,17 +79,10 @@ public: // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; - void finishResolvingWithDetails(std::shared_ptr details); + void finishResolvingWithDetails(ModDetails&& details); protected: - /* If the mod has metadata, this will be filled in the constructor, and passed to - * the ModDetails when calling finishResolvingWithDetails */ - std::shared_ptr m_temp_metadata; - - /* Set the mod status while it doesn't have local details just yet */ - ModStatus m_temp_status = ModStatus::NoMetadata; - - std::shared_ptr m_localDetails; + ModDetails m_local_details; bool m_enabled = true; }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 3e0a7ab0..118b156f 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -46,34 +46,49 @@ enum class ModStatus { Installed, // Both JAR and Metadata are present NotInstalled, // Only the Metadata is present NoMetadata, // Only the JAR is present + Unknown, // Default status }; struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ - QString mod_id; + QString mod_id = {}; /* Human-readable name */ - QString name; + QString name = {}; /* Human-readable mod version */ - QString version; + QString version = {}; /* Human-readable minecraft version */ - QString mcversion; + QString mcversion = {}; /* URL for mod's home page */ - QString homeurl; + QString homeurl = {}; /* Human-readable description */ - QString description; + QString description = {}; /* List of the author's names */ - QStringList authors; + QStringList authors = {}; /* Installation status of the mod */ - ModStatus status; + ModStatus status = ModStatus::Unknown; /* Metadata information, if any */ - std::shared_ptr metadata; + std::shared_ptr metadata = nullptr; + + ModDetails() = default; + + /** Metadata should be handled manually to properly set the mod status. */ + ModDetails(ModDetails& other) + : mod_id(other.mod_id) + , name(other.name) + , version(other.version) + , mcversion(other.mcversion) + , homeurl(other.homeurl) + , description(other.description) + , authors(other.authors) + , status(other.status) + {} }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 8bdab16e..9e07dc89 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -296,7 +296,7 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) auto result = cast_task->result(); if (result && resource) - resource->finishResolvingWithDetails(result->details); + resource->finishResolvingWithDetails(std::move(result->details)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index fe3716ce..8a0273c9 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -20,22 +20,22 @@ namespace { // OLD format: // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc -std::shared_ptr ReadMCModInfo(QByteArray contents) +ModDetails ReadMCModInfo(QByteArray contents) { - auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr + auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { - return nullptr; + return {}; } - std::shared_ptr details = std::make_shared(); + ModDetails details; auto firstObj = arr.at(0).toObject(); - details->mod_id = firstObj.value("modid").toString(); + details.mod_id = firstObj.value("modid").toString(); auto name = firstObj.value("name").toString(); // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name if(name != "Example Mod") { - details->name = name; + details.name = name; } - details->version = firstObj.value("version").toString(); + details.version = firstObj.value("version").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if(!homeurl.isEmpty()) { @@ -45,8 +45,8 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) homeurl.prepend("http://"); } } - details->homeurl = homeurl; - details->description = firstObj.value("description").toString(); + details.homeurl = homeurl; + details.description = firstObj.value("description").toString(); QJsonArray authors = firstObj.value("authorList").toArray(); if (authors.size() == 0) { // FIXME: what is the format of this? is there any? @@ -55,7 +55,7 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) for (auto author: authors) { - details->authors.append(author.toString()); + details.authors.append(author.toString()); } return details; }; @@ -83,7 +83,7 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) { qCritical() << "BAD stuff happened to mod json:"; qCritical() << contents; - return nullptr; + return {}; } auto arrVal = jsonDoc.object().value("modlist"); if(arrVal.isUndefined()) { @@ -94,13 +94,13 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) return getInfoFromArray(arrVal.toArray()); } } - return nullptr; + return {}; } // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md -std::shared_ptr ReadMCModTOML(QByteArray contents) +ModDetails ReadMCModTOML(QByteArray contents) { - std::shared_ptr details = std::make_shared(); + ModDetails details; char errbuf[200]; // top-level table @@ -108,7 +108,7 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) if(!tomlData) { - return nullptr; + return {}; } // array defined by [[mods]] @@ -116,7 +116,7 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) if(!tomlModsArr) { qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; - return nullptr; + return {}; } // we only really care about the first element, since multiple mods in one file is not supported by us at the moment @@ -124,33 +124,33 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) if(!tomlModsTable0) { qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; - return nullptr; + return {}; } // mandatory properties - always in [[mods]] toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); if(modIdDatum.ok) { - details->mod_id = modIdDatum.u.s; + details.mod_id = modIdDatum.u.s; // library says this is required for strings free(modIdDatum.u.s); } toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); if(versionDatum.ok) { - details->version = versionDatum.u.s; + details.version = versionDatum.u.s; free(versionDatum.u.s); } toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); if(displayNameDatum.ok) { - details->name = displayNameDatum.u.s; + details.name = displayNameDatum.u.s; free(displayNameDatum.u.s); } toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); if(descriptionDatum.ok) { - details->description = descriptionDatum.u.s; + details.description = descriptionDatum.u.s; free(descriptionDatum.u.s); } @@ -173,7 +173,7 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) } if(!authors.isEmpty()) { - details->authors.append(authors); + details.authors.append(authors); } toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); @@ -200,7 +200,7 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) homeurl.prepend("http://"); } } - details->homeurl = homeurl; + details.homeurl = homeurl; // this seems to be recursive, so it should free everything toml_free(tomlData); @@ -209,20 +209,20 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) } // https://fabricmc.net/wiki/documentation:fabric_mod_json -std::shared_ptr ReadFabricModInfo(QByteArray contents) +ModDetails ReadFabricModInfo(QByteArray contents) { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; - std::shared_ptr details = std::make_shared(); + ModDetails details; - details->mod_id = object.value("id").toString(); - details->version = object.value("version").toString(); + details.mod_id = object.value("id").toString(); + details.version = object.value("version").toString(); - details->name = object.contains("name") ? object.value("name").toString() : details->mod_id; - details->description = object.value("description").toString(); + details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; + details.description = object.value("description").toString(); if (schemaVersion >= 1) { @@ -230,10 +230,10 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) for (auto author: authors) { if(author.isObject()) { - details->authors.append(author.toObject().value("name").toString()); + details.authors.append(author.toObject().value("name").toString()); } else { - details->authors.append(author.toString()); + details.authors.append(author.toString()); } } @@ -243,7 +243,7 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) if (contact.contains("homepage")) { - details->homeurl = contact.value("homepage").toString(); + details.homeurl = contact.value("homepage").toString(); } } } @@ -251,50 +251,50 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) } // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md -std::shared_ptr ReadQuiltModInfo(QByteArray contents) +ModDetails ReadQuiltModInfo(QByteArray contents) { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); - std::shared_ptr details = std::make_shared(); + ModDetails details; // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md if (schemaVersion == 1) { auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); - details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); - details->version = Json::requireString(modInfo.value("version"), "Mod version"); + details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details.version = Json::requireString(modInfo.value("version"), "Mod version"); auto modMetadata = Json::ensureObject(modInfo.value("metadata")); - details->name = Json::ensureString(modMetadata.value("name"), details->mod_id); - details->description = Json::ensureString(modMetadata.value("description")); + details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); + details.description = Json::ensureString(modMetadata.value("description")); auto modContributors = Json::ensureObject(modMetadata.value("contributors")); // We don't really care about the role of a contributor here - details->authors += modContributors.keys(); + details.authors += modContributors.keys(); auto modContact = Json::ensureObject(modMetadata.value("contact")); if (modContact.contains("homepage")) { - details->homeurl = Json::requireString(modContact.value("homepage")); + details.homeurl = Json::requireString(modContact.value("homepage")); } } return details; } -std::shared_ptr ReadForgeInfo(QByteArray contents) +ModDetails ReadForgeInfo(QByteArray contents) { - std::shared_ptr details = std::make_shared(); + ModDetails details; // Read the data - details->name = "Minecraft Forge"; - details->mod_id = "Forge"; - details->homeurl = "http://www.minecraftforge.net/forum/"; + details.name = "Minecraft Forge"; + details.mod_id = "Forge"; + details.homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; if (!ini.loadFile(contents)) return details; @@ -304,35 +304,35 @@ std::shared_ptr ReadForgeInfo(QByteArray contents) QString revision = ini.get("forge.revision.number", "0").toString(); QString build = ini.get("forge.build.number", "0").toString(); - details->version = major + "." + minor + "." + revision + "." + build; + details.version = major + "." + minor + "." + revision + "." + build; return details; } -std::shared_ptr ReadLiteModInfo(QByteArray contents) +ModDetails ReadLiteModInfo(QByteArray contents) { - std::shared_ptr details = std::make_shared(); + ModDetails details; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); if (object.contains("name")) { - details->mod_id = details->name = object.value("name").toString(); + details.mod_id = details.name = object.value("name").toString(); } if (object.contains("version")) { - details->version = object.value("version").toString(""); + details.version = object.value("version").toString(""); } else { - details->version = object.value("revision").toString(""); + details.version = object.value("revision").toString(""); } - details->mcversion = object.value("mcversion").toString(); + details.mcversion = object.value("mcversion").toString(); auto author = object.value("author").toString(); if(!author.isEmpty()) { - details->authors.append(author); + details.authors.append(author); } - details->description = object.value("description").toString(); - details->homeurl = object.value("url").toString(); + details.description = object.value("description").toString(); + details.homeurl = object.value("url").toString(); return details; } @@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip() file.close(); // to replace ${file.jarVersion} with the actual version, as needed - if (m_result->details && m_result->details->version == "${file.jarVersion}") + if (m_result->details.version == "${file.jarVersion}") { if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { @@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip() manifestVersion = "NONE"; } - m_result->details->version = manifestVersion; + m_result->details.version = manifestVersion; file.close(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index dbecb449..e0a10218 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -13,7 +13,7 @@ class LocalModParseTask : public Task Q_OBJECT public: struct Result { - std::shared_ptr details; + ModDetails details; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 44cb1d5f..a56ba8ab 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -38,8 +38,8 @@ #include "minecraft/mod/MetadataHandler.h" -ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) - : Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) +ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent) + : Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) {} void ModFolderLoadTask::executeTask() diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 86f3f67f..840e95e1 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -57,7 +57,7 @@ public: } public: - ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false); + ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr); void executeTask() override; -- cgit From c3ceefbafbbeb3d31630ef329405ebaacdf9fce5 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 12 Aug 2022 17:09:56 -0300 Subject: refactor+fix: add new tests for Resource models and fix issues Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel.cpp | 18 +- launcher/minecraft/mod/ModFolderModel_test.cpp | 119 ----------- launcher/minecraft/mod/ResourceFolderModel.cpp | 46 +++-- launcher/minecraft/mod/ResourceFolderModel.h | 40 ++-- .../minecraft/mod/ResourceFolderModel_test.cpp | 219 +++++++++++++++++++++ launcher/minecraft/mod/tasks/BasicFolderLoadTask.h | 13 +- launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 12 +- launcher/minecraft/mod/tasks/LocalModParseTask.h | 5 + launcher/minecraft/mod/testdata/supercoolmod.jar | 1 + 9 files changed, 314 insertions(+), 159 deletions(-) delete mode 100644 launcher/minecraft/mod/ModFolderModel_test.cpp create mode 100644 launcher/minecraft/mod/ResourceFolderModel_test.cpp create mode 100644 launcher/minecraft/mod/testdata/supercoolmod.jar diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 9e07dc89..15c713b8 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -165,7 +165,7 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const Task* ModFolderModel::createUpdateTask() { auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this); m_first_folder_load = false; return task; } @@ -181,6 +181,9 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat if(mod->fileinfo().fileName() == filename){ auto index_dir = indexDir(); mod->destroy(index_dir, preserve_metadata); + + update(); + return true; } } @@ -206,6 +209,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) auto index_dir = indexDir(); m->destroy(index_dir); } + + update(); + return true; } @@ -268,14 +274,13 @@ void ModFolderModel::onUpdateSucceeded() applyUpdates(current_set, new_set, new_mods); - update_results.reset(); m_current_update_task.reset(); - emit updateFinished(); - - if(m_scheduled_update) { + if (m_scheduled_update) { m_scheduled_update = false; update(); + } else { + emit updateFinished(); } } @@ -299,9 +304,6 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) resource->finishResolvingWithDetails(std::move(result->details)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); - - parse_task->deleteLater(); - m_active_parse_tasks.remove(ticket); } diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp deleted file mode 100644 index 1b50ebd6..00000000 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* -* PolyMC - Minecraft Launcher -* Copyright (C) 2022 Sefa Eyeoglu -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, version 3. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -* -* This file incorporates work covered by the following copyright and -* permission notice: -* -* Copyright 2013-2021 MultiMC Contributors -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -#include -#include -#include - -#include "FileSystem.h" -#include "minecraft/mod/ModFolderModel.h" - -class ModFolderModelTest : public QObject -{ - Q_OBJECT - -private -slots: - // test for GH-1178 - install a folder with files to a mod list - void test_1178() - { - // source - QString source = QFINDTESTDATA("testdata/test_folder"); - - // sanity check - QVERIFY(!source.endsWith('/')); - - auto verify = [](QString path) - { - QDir target_dir(FS::PathCombine(path, "test_folder")); - QVERIFY(target_dir.entryList().contains("pack.mcmeta")); - QVERIFY(target_dir.entryList().contains("assets")); - }; - - // 1. test with no trailing / - { - QString folder = source; - QTemporaryDir tempDir; - - QEventLoop loop; - - ModFolderModel m(tempDir.path(), true); - - connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); - - QTimer expire_timer; - expire_timer.callOnTimeout(&loop, &QEventLoop::quit); - expire_timer.setSingleShot(true); - expire_timer.start(4000); - - m.installMod(folder); - - loop.exec(); - - QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); - expire_timer.stop(); - - verify(tempDir.path()); - } - - // 2. test with trailing / - { - QString folder = source + '/'; - QTemporaryDir tempDir; - QEventLoop loop; - ModFolderModel m(tempDir.path(), true); - - connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); - - QTimer expire_timer; - expire_timer.callOnTimeout(&loop, &QEventLoop::quit); - expire_timer.setSingleShot(true); - expire_timer.start(4000); - - m.installMod(folder); - - loop.exec(); - - QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); - expire_timer.stop(); - - verify(tempDir.path()); - } - } -}; - -QTEST_GUILESS_MAIN(ModFolderModelTest) - -#include "ModFolderModel_test.moc" diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index c27a5e2d..b7213c47 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -24,8 +24,6 @@ bool ResourceFolderModel::startWatching(const QStringList paths) if (m_is_watching) return false; - update(); - auto couldnt_be_watched = m_watcher.addPaths(paths); for (auto path : paths) { if (couldnt_be_watched.contains(path)) @@ -34,6 +32,8 @@ bool ResourceFolderModel::startWatching(const QStringList paths) qDebug() << "Started watching " << path; } + update(); + m_is_watching = !m_is_watching; return m_is_watching; } @@ -105,7 +105,8 @@ bool ResourceFolderModel::installResource(QString original_path) QFileInfo new_path_file_info(new_path); resource.setFile(new_path_file_info); - update(); + if (!m_is_watching) + return update(); return true; } @@ -123,7 +124,8 @@ bool ResourceFolderModel::installResource(QString original_path) QFileInfo newpathInfo(new_path); resource.setFile(newpathInfo); - update(); + if (!m_is_watching) + return update(); return true; } @@ -136,8 +138,13 @@ bool ResourceFolderModel::installResource(QString original_path) bool ResourceFolderModel::uninstallResource(QString file_name) { for (auto& resource : m_resources) { - if (resource->fileinfo().fileName() == file_name) - return resource->destroy(); + if (resource->fileinfo().fileName() == file_name) { + auto res = resource->destroy(); + + update(); + + return res; + } } return false; } @@ -156,13 +163,21 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) } auto& resource = m_resources.at(i.row()); + resource->destroy(); } + + update(); + return true; } +static QMutex s_update_task_mutex; bool ResourceFolderModel::update() { + // We hold a lock here to prevent race conditions on the m_current_update_task reset. + QMutexLocker lock(&s_update_task_mutex); + // Already updating, so we schedule a future update and return. if (m_current_update_task) { m_scheduled_update = true; @@ -183,7 +198,7 @@ bool ResourceFolderModel::update() return true; } -void ResourceFolderModel::resolveResource(Resource::WeakPtr res) +void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; @@ -205,6 +220,8 @@ void ResourceFolderModel::resolveResource(Resource::WeakPtr res) task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + connect( + task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); auto* thread_pool = QThreadPool::globalInstance(); thread_pool->start(task); @@ -229,15 +246,13 @@ void ResourceFolderModel::onUpdateSucceeded() applyUpdates(current_set, new_set, new_resources); - update_results.reset(); - m_current_update_task->deleteLater(); m_current_update_task.reset(); - emit updateFinished(); - if (m_scheduled_update) { m_scheduled_update = false; update(); + } else { + emit updateFinished(); } } @@ -247,9 +262,6 @@ void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) if (iter == m_active_parse_tasks.constEnd()) return; - (*iter)->deleteLater(); - m_active_parse_tasks.remove(ticket); - int row = m_resources_index[resource_id]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } @@ -259,6 +271,12 @@ Task* ResourceFolderModel::createUpdateTask() return new BasicFolderLoadTask(m_dir); } + +bool ResourceFolderModel::hasPendingParseTasks() const +{ + return !m_active_parse_tasks.isEmpty(); +} + void ResourceFolderModel::directoryChanged(QString path) { update(); diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 59d2388a..b3a474ba 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -62,7 +62,7 @@ class ResourceFolderModel : public QAbstractListModel { virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ - virtual void resolveResource(Resource::WeakPtr res); + virtual void resolveResource(Resource::Ptr res); [[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] bool empty() const { return size() == 0; } @@ -71,6 +71,13 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] QDir const& dir() const { return m_dir; } + /** Checks whether there's any parse tasks being done. + * + * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having + * such tasks would introduce an undefined behavior, most likely resulting in a crash. + */ + [[nodiscard]] bool hasPendingParseTasks() const; + /* Qt behavior */ /* Basic columns */ @@ -228,10 +235,12 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet QSet kept_set = current_set; kept_set.intersect(new_set); - for (auto& kept : kept_set) { - auto row = m_resources_index[kept]; + for (auto const& kept : kept_set) { + auto row_it = m_resources_index.constFind(kept); + Q_ASSERT(row_it != m_resources_index.constEnd()); + auto row = row_it.value(); - auto new_resource = new_resources[kept]; + auto& new_resource = new_resources[kept]; auto const& current_resource = m_resources[row]; if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { @@ -242,11 +251,12 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. if (current_resource->isResolving()) { - m_active_parse_tasks.remove(current_resource->resolutionTicket()); + auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get(); + task->abort(); } - m_resources[row] = new_resource; - resolveResource(new_resource); + m_resources[row].reset(new_resource); + resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } @@ -260,21 +270,21 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet for (auto& removed : removed_set) removed_rows.append(m_resources_index[removed]); - std::sort(removed_rows.begin(), removed_rows.end()); - - for (int i = 0; i < removed_rows.size(); i++) - removed_rows[i] -= i; + std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); for (auto& removed_index : removed_rows) { - beginRemoveRows(QModelIndex(), removed_index, removed_index); - auto removed_it = m_resources.begin() + removed_index; + + Q_ASSERT(removed_it != m_resources.end()); + Q_ASSERT(removed_set.contains(removed_it->get()->internal_id())); + if ((*removed_it)->isResolving()) { - m_active_parse_tasks.remove((*removed_it)->resolutionTicket()); + auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get(); + task->abort(); } + beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); - endRemoveRows(); } } diff --git a/launcher/minecraft/mod/ResourceFolderModel_test.cpp b/launcher/minecraft/mod/ResourceFolderModel_test.cpp new file mode 100644 index 00000000..5e29e6aa --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel_test.cpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include + +#include "FileSystem.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" + +#define EXEC_UPDATE_TASK(EXEC, VERIFY) \ + QEventLoop loop; \ + \ + connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \ + \ + QTimer expire_timer; \ + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ + expire_timer.setSingleShot(true); \ + expire_timer.start(4000); \ + \ + VERIFY(EXEC); \ + loop.exec(); \ + \ + QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \ + expire_timer.stop(); \ + \ + disconnect(&model, nullptr, nullptr, nullptr); + +class ResourceFolderModelTest : public QObject +{ + Q_OBJECT + +private +slots: + // test for GH-1178 - install a folder with files to a mod list + void test_1178() + { + // source + QString source = QFINDTESTDATA("testdata/test_folder"); + + // sanity check + QVERIFY(!source.endsWith('/')); + + auto verify = [](QString path) + { + QDir target_dir(FS::PathCombine(path, "test_folder")); + QVERIFY(target_dir.entryList().contains("pack.mcmeta")); + QVERIFY(target_dir.entryList().contains("assets")); + }; + + // 1. test with no trailing / + { + QString folder = source; + QTemporaryDir tempDir; + + QEventLoop loop; + + ModFolderModel m(tempDir.path(), true); + + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); + + QTimer expire_timer; + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); + expire_timer.setSingleShot(true); + expire_timer.start(4000); + + m.installMod(folder); + + loop.exec(); + + QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); + expire_timer.stop(); + + verify(tempDir.path()); + } + + // 2. test with trailing / + { + QString folder = source + '/'; + QTemporaryDir tempDir; + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); + + QTimer expire_timer; + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); + expire_timer.setSingleShot(true); + expire_timer.start(4000); + + m.installMod(folder); + + loop.exec(); + + QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); + expire_timer.stop(); + + verify(tempDir.path()); + } + } + + void test_addFromWatch() + { + QString source = QFINDTESTDATA("testdata"); + + ModFolderModel model(source); + + QCOMPARE(model.size(), 0); + + EXEC_UPDATE_TASK(model.startWatching(), ) + + for (auto mod : model.allMods()) + qDebug() << mod->name(); + + QCOMPARE(model.size(), 2); + + model.stopWatching(); + + while (model.hasPendingParseTasks()) { + QTest::qSleep(20); + QCoreApplication::processEvents(); + } + } + + void test_removeResource() + { + QString folder_resource = QFINDTESTDATA("testdata/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar"); + + QTemporaryDir tmp; + + ResourceFolderModel model(QDir(tmp.path())); + + QCOMPARE(model.size(), 0); + + { + EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) + } + + QCOMPARE(model.size(), 1); + qDebug() << "Added first mod."; + + { + EXEC_UPDATE_TASK(model.startWatching(), ) + } + + QCOMPARE(model.size(), 1); + qDebug() << "Started watching the temp folder."; + + { + EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) + } + + QCOMPARE(model.size(), 2); + qDebug() << "Added second mod."; + + { + EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY); + } + + QCOMPARE(model.size(), 1); + qDebug() << "Removed first mod."; + + QString mod_file_name {model.at(0).fileinfo().fileName()}; + QVERIFY(!mod_file_name.isEmpty()); + + { + EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY); + } + + QCOMPARE(model.size(), 0); + qDebug() << "Removed second mod."; + + model.stopWatching(); + + while (model.hasPendingParseTasks()) { + QTest::qSleep(20); + QCoreApplication::processEvents(); + } + } +}; + +QTEST_GUILESS_MAIN(ResourceFolderModelTest) + +#include "ResourceFolderModel_test.moc" diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index 2944c747..cc02a9b9 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -17,7 +17,7 @@ class BasicFolderLoadTask : public Task Q_OBJECT public: struct Result { - QMap resources; + QMap resources; }; using ResultPtr = std::shared_ptr; @@ -27,6 +27,10 @@ public: public: BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {} + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override { m_aborted = true; return true; } + void executeTask() override { m_dir.refresh(); @@ -35,10 +39,15 @@ public: m_result->resources.insert(resource->internal_id(), resource); } - emitSucceeded(); + if (m_aborted) + emitAborted(); + else + emitSucceeded(); } private: QDir m_dir; ResultPtr m_result; + + bool m_aborted = false; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 8a0273c9..c486bd46 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -497,6 +497,12 @@ void LocalModParseTask::processAsLitemod() zip.close(); } +bool LocalModParseTask::abort() +{ + m_aborted = true; + return true; +} + void LocalModParseTask::executeTask() { switch(m_type) @@ -513,5 +519,9 @@ void LocalModParseTask::executeTask() default: break; } - emitSucceeded(); + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index e0a10218..4bbf3c85 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -20,6 +20,9 @@ public: return m_result; } + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile); void executeTask() override; @@ -35,4 +38,6 @@ private: ResourceType m_type; QFileInfo m_modFile; ResultPtr m_result; + + bool m_aborted = false; }; diff --git a/launcher/minecraft/mod/testdata/supercoolmod.jar b/launcher/minecraft/mod/testdata/supercoolmod.jar new file mode 100644 index 00000000..d8cf9860 --- /dev/null +++ b/launcher/minecraft/mod/testdata/supercoolmod.jar @@ -0,0 +1 @@ +the best mod. -- cgit From e2ab2aea32b6d48ee0c9f90442ed53c47e2e7d96 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 13 Aug 2022 11:58:39 -0300 Subject: change: add enable/disable to resources TIL that zip resource packs, when disabled, actually have the effect of not showing up in the game at all. Since this can be useful to someone, I moved the logic for it to the resources. Signed-off-by: flow --- launcher/minecraft/mod/Mod.cpp | 35 ---------- launcher/minecraft/mod/Mod.h | 6 -- launcher/minecraft/mod/ModFolderModel.cpp | 76 ---------------------- launcher/minecraft/mod/ModFolderModel.h | 7 -- launcher/minecraft/mod/Resource.cpp | 57 +++++++++++++++- launcher/minecraft/mod/Resource.h | 16 +++++ launcher/minecraft/mod/ResourceFolderModel.cpp | 64 +++++++++++++++++- launcher/minecraft/mod/ResourceFolderModel.h | 14 ++-- .../minecraft/mod/ResourceFolderModel_test.cpp | 56 ++++++++++++++++ .../ui/pages/instance/ExternalResourcesPage.cpp | 28 ++++++++ launcher/ui/pages/instance/ExternalResourcesPage.h | 6 +- launcher/ui/pages/instance/ModFolderPage.cpp | 29 --------- launcher/ui/pages/instance/ModFolderPage.h | 5 -- 13 files changed, 231 insertions(+), 168 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 5e186471..39023f69 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -56,37 +56,6 @@ Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) m_local_details.metadata = std::make_shared(std::move(metadata)); } -auto Mod::enable(bool value) -> bool -{ - if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) - return false; - - if (m_enabled == value) - return false; - - QString path = m_file_info.absoluteFilePath(); - QFile file(path); - if (value) { - if (!path.endsWith(".disabled")) - return false; - path.chop(9); - - if (!file.rename(path)) - return false; - } else { - path += ".disabled"; - - if (!file.rename(path)) - return false; - } - - if (status() == ModStatus::NoMetadata) - setFile(QFileInfo(path)); - - m_enabled = value; - return true; -} - void Mod::setStatus(ModStatus status) { m_local_details.status = status; @@ -108,10 +77,6 @@ std::pair Mod::compare(const Resource& other, SortType type) const switch (type) { default: case SortType::ENABLED: - if (enabled() && !cast_other->enabled()) - return { 1, type == SortType::ENABLED }; - if (!enabled() && cast_other->enabled()) - return { -1, type == SortType::ENABLED }; case SortType::NAME: case SortType::DATE: { auto res = Resource::compare(other, type); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index b9b57058..f336bec4 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -54,8 +54,6 @@ public: Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); Mod(QString file_path) : Mod(QFileInfo(file_path)) {} - auto enabled() const -> bool { return m_enabled; } - auto details() const -> const ModDetails&; auto name() const -> QString override; auto version() const -> QString; @@ -71,8 +69,6 @@ public: void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } - auto enable(bool value) -> bool; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; @@ -83,6 +79,4 @@ public: protected: ModDetails m_local_details; - - bool m_enabled = true; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 15c713b8..4e264a74 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -104,20 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } } -bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) - { - return false; - } - - if (role == Qt::CheckStateRole) - { - return setModStatus(index.row(), Toggle); - } - return false; -} - QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { switch (role) @@ -305,65 +291,3 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } - - -bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) -{ - if(!m_can_interact) { - return false; - } - - if(indexes.isEmpty()) - return true; - - for (auto index: indexes) - { - if(index.column() != 0) { - continue; - } - setModStatus(index.row(), enable); - } - return true; -} - -bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) -{ - if(row < 0 || row >= m_resources.size()) { - return false; - } - - auto mod = at(row); - bool desiredStatus; - switch(action) { - case Enable: - desiredStatus = true; - break; - case Disable: - desiredStatus = false; - break; - case Toggle: - default: - desiredStatus = !mod->enabled(); - break; - } - - if(desiredStatus == mod->enabled()) { - return true; - } - - // preserve the row, but change its ID - auto oldId = mod->internal_id(); - if(!mod->enable(!mod->enabled())) { - return false; - } - auto newId = mod->internal_id(); - if(m_resources_index.contains(newId)) { - // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled - // But is it necessary? - } - m_resources_index.remove(oldId); - m_resources_index[newId] = row; - emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); - return true; -} - diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 7fe830c2..c33195ed 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -77,7 +77,6 @@ public: ModFolderModel(const QString &dir, bool is_indexed = false); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex &parent) const override; @@ -91,9 +90,6 @@ public: /// Deletes all the selected mods bool deleteMods(const QModelIndexList &indexes); - /// Enable or disable listed mods - bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); - bool isValid(); bool startWatching() override; @@ -111,9 +107,6 @@ slots: void onUpdateSucceeded() override; void onParseSucceeded(int ticket, QString resource_id) override; -private: - bool setModStatus(int index, ModStatusAction action); - protected: bool m_is_indexed; bool m_first_folder_load = true; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index c58df3d8..0fbcfd7c 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -29,8 +29,10 @@ void Resource::parseFile() m_type = ResourceType::FOLDER; m_name = file_name; } else if (m_file_info.isFile()) { - if (file_name.endsWith(".disabled")) + if (file_name.endsWith(".disabled")) { file_name.chop(9); + m_enabled = false; + } if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { m_type = ResourceType::ZIPFILE; @@ -59,6 +61,11 @@ std::pair Resource::compare(const Resource& other, SortType type) con { switch (type) { default: + case SortType::ENABLED: + if (enabled() && !other.enabled()) + return { 1, type == SortType::ENABLED }; + if (!enabled() && other.enabled()) + return { -1, type == SortType::ENABLED }; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; @@ -85,6 +92,54 @@ bool Resource::applyFilter(QRegularExpression filter) const return filter.match(name()).hasMatch(); } +bool Resource::enable(EnableAction action) +{ + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) + return false; + + + QString path = m_file_info.absoluteFilePath(); + QFile file(path); + + bool enable = true; + switch (action) { + case EnableAction::ENABLE: + enable = true; + break; + case EnableAction::DISABLE: + enable = false; + break; + case EnableAction::TOGGLE: + default: + enable = !enabled(); + break; + } + + if (m_enabled == enable) + return false; + + if (enable) { + // m_enabled is false, but there's no '.disabled' suffix. + // TODO: Report error? + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + + if (!file.rename(path)) + return false; + } else { + path += ".disabled"; + + if (!file.rename(path)) + return false; + } + + setFile(QFileInfo(path)); + + m_enabled = enable; + return true; +} + bool Resource::destroy() { m_type = ResourceType::UNKNOWN; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index e76bc49e..cee1f172 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -22,6 +22,12 @@ enum class SortType { ENABLED, }; +enum class EnableAction { + ENABLE, + DISABLE, + TOGGLE +}; + /** General class for managed resources. It mirrors a file in disk, with some more info * for display and house-keeping purposes. * @@ -47,6 +53,7 @@ class Resource : public QObject { [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } [[nodiscard]] auto type() const -> ResourceType { return m_type; } + [[nodiscard]] bool enabled() const { return m_enabled; } [[nodiscard]] virtual auto name() const -> QString { return m_name; } [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } @@ -65,6 +72,12 @@ class Resource : public QObject { */ [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const; + /** Changes the enabled property, according to 'action'. + * + * Returns whether a change was applied to the Resource's properties. + */ + bool enable(EnableAction action); + [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } @@ -92,6 +105,9 @@ class Resource : public QObject { /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; + /* Whether the resource is enabled (e.g. shows up in the game) or not. */ + bool m_enabled = true; + /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; bool m_is_resolved = false; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index b7213c47..31d88eb6 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -172,6 +172,44 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; } +bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action) +{ + if (!m_can_interact) + return false; + + if (indexes.isEmpty()) + return true; + + bool succeeded = true; + for (auto const& idx : indexes) { + if (!validateIndex(idx) || idx.column() != 0) + continue; + + int row = idx.row(); + + auto& resource = m_resources[row]; + + // Preserve the row, but change its ID + auto old_id = resource->internal_id(); + if (!resource->enable(action)) { + succeeded = false; + continue; + } + + auto new_id = resource->internal_id(); + if (m_resources_index.contains(new_id)) { + // FIXME: https://github.com/PolyMC/PolyMC/issues/550 + } + + m_resources_index.remove(old_id); + m_resources_index[new_id] = row; + + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + + return succeeded; +} + static QMutex s_update_task_mutex; bool ResourceFolderModel::update() { @@ -271,7 +309,6 @@ Task* ResourceFolderModel::createUpdateTask() return new BasicFolderLoadTask(m_dir); } - bool ResourceFolderModel::hasPendingParseTasks() const { return !m_active_parse_tasks.isEmpty(); @@ -370,11 +407,30 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const } case Qt::ToolTipRole: return m_resources[row]->internal_id(); + case Qt::CheckStateRole: + switch (column) { + case ACTIVE_COLUMN: + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + default: + return {}; + } default: return {}; } } +bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + int row = index.row(); + if (row < 0 || row >= rowCount(index) || !index.isValid()) + return false; + + if (role == Qt::CheckStateRole) + return setResourceEnabled({ index }, EnableAction::TOGGLE); + + return false; +} + QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { switch (role) { @@ -389,9 +445,14 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio } case Qt::ToolTipRole: { switch (section) { + case ACTIVE_COLUMN: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("Is the resource enabled?"); case NAME_COLUMN: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The name of the resource."); case DATE_COLUMN: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The date and time this resource was last changed (or added)."); default: return {}; @@ -459,4 +520,3 @@ void ResourceFolderModel::enableInteraction(bool enabled) return (compare_result.first < 0); return (compare_result.first > 0); } - diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index b3a474ba..e27b5db6 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -55,9 +55,14 @@ class ResourceFolderModel : public QAbstractListModel { * Returns whether the removal was successful. */ virtual bool uninstallResource(QString file_name); - virtual bool deleteResources(const QModelIndexList&); + /** Applies the given 'action' to the resources in 'indexes'. + * + * Returns whether the action was successfully applied to all resources. + */ + virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); + /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ virtual bool update(); @@ -66,6 +71,7 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] bool empty() const { return size() == 0; } + [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); } [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } [[nodiscard]] QList const& all() const { return m_resources; } @@ -81,7 +87,7 @@ class ResourceFolderModel : public QAbstractListModel { /* Qt behavior */ /* Basic columns */ - enum Columns { NAME_COLUMN = 0, DATE_COLUMN, NUM_COLUMNS }; + enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); } [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; }; @@ -96,7 +102,7 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool validateIndex(const QModelIndex& index) const; [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return false; }; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; @@ -174,7 +180,7 @@ class ResourceFolderModel : public QAbstractListModel { protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::NAME, SortType::DATE }; + QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; bool m_can_interact = true; diff --git a/launcher/minecraft/mod/ResourceFolderModel_test.cpp b/launcher/minecraft/mod/ResourceFolderModel_test.cpp index 5e29e6aa..fe98552e 100644 --- a/launcher/minecraft/mod/ResourceFolderModel_test.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel_test.cpp @@ -212,6 +212,62 @@ slots: QCoreApplication::processEvents(); } } + + void test_enable_disable() + { + QString folder_resource = QFINDTESTDATA("testdata/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar"); + + QTemporaryDir tmp; + ResourceFolderModel model(tmp.path()); + + QCOMPARE(model.size(), 0); + + { + EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) + } + { + EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) + } + + for (auto res : model.all()) + qDebug() << res->name(); + + QCOMPARE(model.size(), 2); + + auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1); + auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1); + auto id_1 = res_1.internal_id(); + auto id_2 = res_2.internal_id(); + bool initial_enabled_res_2 = res_2.enabled(); + bool initial_enabled_res_1 = res_1.enabled(); + + QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN); + qDebug() << "res_1 is of the correct type."; + QVERIFY(res_1.enabled()); + qDebug() << "res_1 is initially enabled."; + + QVERIFY(res_1.enable(EnableAction::TOGGLE)); + + QVERIFY(res_1.enabled() == !initial_enabled_res_1); + qDebug() << "res_1 got successfully toggled."; + + QVERIFY(res_1.enable(EnableAction::TOGGLE)); + qDebug() << "res_1 got successfully toggled again."; + + QVERIFY(res_1.enabled() == initial_enabled_res_1); + QVERIFY(res_1.internal_id() == id_1); + qDebug() << "res_1 got back to its initial state."; + + QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE)); + QVERIFY(res_2.enabled() == initial_enabled_res_2); + QVERIFY(res_2.internal_id() == id_2); + + while (model.hasPendingParseTasks()) { + QTest::qSleep(20); + QCoreApplication::processEvents(); + } + } }; QTEST_GUILESS_MAIN(ResourceFolderModelTest) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 0a3687e5..f31e8325 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -40,6 +40,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); connect(ui->treeView, &ModListView::customContextMenuRequested, this, &ExternalResourcesPage::ShowContextMenu); + connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); @@ -81,6 +82,15 @@ void ExternalResourcesPage::retranslate() ui->retranslateUi(this); } +void ExternalResourcesPage::itemActivated(const QModelIndex&) +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); +} + void ExternalResourcesPage::filterTextChanged(const QString& newContents) { m_viewFilter = newContents; @@ -157,6 +167,24 @@ void ExternalResourcesPage::removeItem() m_model->deleteResources(selection.indexes()); } +void ExternalResourcesPage::enableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE); +} + +void ExternalResourcesPage::disableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); +} + void ExternalResourcesPage::viewConfigs() { DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 280f1542..8e352cef 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -45,15 +45,15 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous); protected slots: - virtual void itemActivated(const QModelIndex& index) {}; + void itemActivated(const QModelIndex& index); void filterTextChanged(const QString& newContents); virtual void runningStateChanged(bool running); virtual void addItem(); virtual void removeItem(); - virtual void enableItem() {}; - virtual void disableItem() {}; + virtual void enableItem(); + virtual void disableItem(); virtual void viewFolder(); virtual void viewConfigs(); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 63897fb0..75b40e77 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -110,8 +110,6 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); } - - connect(ui->treeView, &ModListView::activated, this, &ModFolderPage::itemActivated); } void ModFolderPage::runningStateChanged(bool running) @@ -126,33 +124,6 @@ bool ModFolderPage::shouldDisplay() const return true; } -void ModFolderPage::itemActivated(const QModelIndex&) -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle); -} - -void ModFolderPage::enableItem() -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Enable); -} - -void ModFolderPage::disableItem() -{ - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setModStatus(selection.indexes(), ModFolderModel::Disable); -} - bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 5da353f0..7fc9d9a1 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -58,11 +58,6 @@ class ModFolderPage : public ExternalResourcesPage { public slots: bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; - void itemActivated(const QModelIndex& index) override; - - void enableItem() override; - void disableItem() override; - private slots: void installMods(); void updateMods(); -- cgit From 0b81b283bfdd16b409127f22eac7b51ce0142929 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 13 Aug 2022 14:35:44 -0300 Subject: fix: LGTM warnings Signed-off-by: flow --- launcher/minecraft/mod/ModDetails.h | 28 ++++++++++++++++++++++++++ launcher/minecraft/mod/ResourceFolderModel.cpp | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 118b156f..dd84b0a3 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -91,4 +91,32 @@ struct ModDetails , authors(other.authors) , status(other.status) {} + + ModDetails& operator=(ModDetails& other) + { + this->mod_id = other.mod_id; + this->name = other.name; + this->version = other.version; + this->mcversion = other.mcversion; + this->homeurl = other.homeurl; + this->description = other.description; + this->authors = other.authors; + this->status = other.status; + + return *this; + } + + ModDetails& operator=(ModDetails&& other) + { + this->mod_id = other.mod_id; + this->name = other.name; + this->version = other.version; + this->mcversion = other.mcversion; + this->homeurl = other.homeurl; + this->description = other.description; + this->authors = other.authors; + this->status = other.status; + + return *this; + } }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 31d88eb6..bc18ddc2 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -380,8 +380,8 @@ bool ResourceFolderModel::validateIndex(const QModelIndex& index) const if (!index.isValid()) return false; - size_t row = index.row(); - if (row < 0 || row >= size()) + int row = index.row(); + if (row < 0 || row >= m_resources.size()) return false; return true; -- cgit