From 8cf41be77faec90c8d33d80f49c3e104217bc243 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 2 May 2025 21:53:20 +0300 Subject: [PATCH 01/18] feat: decode dependencies from mod jar Signed-off-by: Trial97 --- launcher/minecraft/mod/Mod.cpp | 5 + launcher/minecraft/mod/Mod.h | 1 + launcher/minecraft/mod/ModDetails.h | 3 + .../minecraft/mod/tasks/LocalModParseTask.cpp | 101 ++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 32b940c5b..fd3b5cb46 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -283,3 +283,8 @@ bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } + +QStringList Mod::dependencies() const +{ + return details().dependencies; +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index c548f5350..7bac7d0f4 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -70,6 +70,7 @@ class Mod : public Resource { auto loaders() const -> QString; auto mcVersions() const -> QString; auto releaseType() const -> QString; + QStringList dependencies() const; /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 9b81f561f..02cf42a38 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -138,6 +138,8 @@ struct ModDetails { /* Path of mod logo */ QString icon_file = {}; + QStringList dependencies = {}; + ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ @@ -152,6 +154,7 @@ struct ModDetails { , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) + , dependencies(other.dependencies) {} ModDetails& operator=(const ModDetails& other) = default; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 59d3876b3..c82392117 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -61,6 +61,36 @@ ModDetails ReadMCModInfo(QByteArray contents) for (auto author : authors) { details.authors.append(author.toString()); } + + if (details.mod_id.startsWith("mod_")) { + details.mod_id = details.mod_id.mid(4); + } + + auto addDep = [&details](QString dep) { + if (dep == "mod_MinecraftForge" || dep == "Forge") + return; + if (dep.contains(":")) { + dep = dep.section(":", 1); + } + if (dep.contains("@")) { + dep = dep.section("@", 0, 0); + } + if (dep.startsWith("mod_")) { + dep = dep.mid(4); + } + details.dependencies.append(dep); + }; + + if (firstObj.contains("requiredMods")) { + for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + addDep(dep.toString()); + } + } else if (firstObj.contains("dependencies")) { + for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + addDep(dep.toString()); + } + } + return details; }; QJsonParseError jsonError; @@ -198,6 +228,42 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.icon_file = logoFile; + auto parseDep = [&details](toml::array* dependencies) { + if (dependencies) { + for (auto& dep : *dependencies) { + auto dep_table = dep.as_table(); + if (dep_table) { + auto modId = dep_table->get("modId")->value_or(""); + if (modId != "forge" && modId != "neoforge" && modId != "minecraft") { + if (dep_table->contains("type") && (dep_table->get("type"))->value_or("") == "required") { + details.dependencies.append(QString::fromStdString(modId)); + } else if (dep_table->contains("mandatory") && (dep_table->get("mandatory"))->value_or(false)) { + details.dependencies.append(QString::fromStdString(modId)); + } + } + } + } + } + }; + + if (tomlData.contains("dependencies")) { + auto depValue = tomlData["dependencies"]; + if (auto array = depValue.as_array()) { + parseDep(array); + } else if (auto depTable = depValue.as_table()) { + auto expectedKey = details.mod_id.toStdString(); + if (!depTable->contains(expectedKey)) { + for (auto [k, v] : *depTable) { + expectedKey = k; + break; + } + } + if (auto array = (*depTable)[expectedKey].as_array()) { + parseDep(array); + } + } + } + return details; } @@ -285,6 +351,18 @@ ModDetails ReadFabricModInfo(QByteArray contents) details.icon_file = icon.toString(); } } + + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isObject()) { + auto obj = depends.toObject(); + for (auto key : obj.keys()) { + if (key != "fabricloader" && key != "minecraft" && !key.startsWith("fabric-")) { + details.dependencies.append(key); + } + } + } + } } return details; } @@ -372,6 +450,29 @@ ModDetails ReadQuiltModInfo(QByteArray contents) details.icon_file = icon.toString(); } } + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isArray()) { + auto array = depends.toArray(); + for (auto obj : array) { + QString modId; + if (obj.isString()) { + modId = obj.toString(); + } else if (obj.isObject()) { + auto objValue = obj.toObject(); + modId = objValue.value("id").toString(); + if (objValue.contains("optional") && objValue.value("optional").toBool()) { + continue; + } + } else { + continue; + } + if (modId != "minecraft" && !modId.startsWith("quilt_")) { + details.dependencies.append(modId); + } + } + } + } } } catch (const Exception& e) { From bacce134c0950b5678e7879967a19cf46e3be36a Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 3 May 2025 12:25:18 +0300 Subject: [PATCH 02/18] feat: store provider dependencies Signed-off-by: Trial97 --- launcher/minecraft/mod/Resource.h | 4 --- launcher/modplatform/ModIndex.cpp | 36 ++++++++++++++++++++++++ launcher/modplatform/ModIndex.h | 5 ++++ launcher/modplatform/packwiz/Packwiz.cpp | 27 ++++++++++++++++++ launcher/modplatform/packwiz/Packwiz.h | 2 ++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 87bfd4345..242b8a30d 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -152,9 +152,6 @@ class Resource : public QObject { bool isMoreThanOneHardLink() const; - auto mod_id() const -> QString { return m_mod_id; } - void setModId(const QString& modId) { m_mod_id = modId; } - protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; @@ -165,7 +162,6 @@ class Resource : public QObject { 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; - QString m_mod_id; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 7b20d37ec..e365fa801 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -178,4 +178,40 @@ Side SideUtils::fromString(QString side) return Side::UniversalSide; return Side::UniversalSide; } + +QString DependencyTypeUtils::toString(DependencyType type) +{ + switch (type) { + case DependencyType::REQUIRED: + return "REQUIRED"; + case DependencyType::OPTIONAL: + return "OPTIONAL"; + case DependencyType::INCOMPATIBLE: + return "INCOMPATIBLE"; + case DependencyType::EMBEDDED: + return "EMBEDDED"; + case DependencyType::TOOL: + return "TOOL"; + case DependencyType::INCLUDE: + return "INCLUDE"; + case DependencyType::UNKNOWN: + return "UNKNOWN"; + } + return "UNKNOWN"; +} + +DependencyType DependencyTypeUtils::fromString(const QString& str) +{ + static const QHash map = { + { "REQUIRED", DependencyType::REQUIRED }, + { "OPTIONAL", DependencyType::OPTIONAL }, + { "INCOMPATIBLE", DependencyType::INCOMPATIBLE }, + { "EMBEDDED", DependencyType::EMBEDDED }, + { "TOOL", DependencyType::TOOL }, + { "INCLUDE", DependencyType::INCLUDE }, + { "UNKNOWN", DependencyType::UNKNOWN }, + }; + + return map.value(str.toUpper(), DependencyType::UNKNOWN); +} } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 5cde2274f..f225208da 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -58,6 +58,11 @@ QString toString(Side side); Side fromString(QString side); } // namespace SideUtils +namespace DependencyTypeUtils { +QString toString(DependencyType type); +DependencyType fromString(const QString& str); +} // namespace DependencyTypeUtils + namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 83ca0f4f2..a14466629 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -122,6 +122,7 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; + mod.dependencies = mod_version.dependencies; return mod; } @@ -190,6 +191,16 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) return; } + toml::array deps; + for (auto dep : mod.dependencies) { + auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() }, + { "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } }; + if (!dep.version.isEmpty()) { + tbl.emplace("version", dep.version.toStdString()); + } + deps.push_back(tbl); + } + // Put TOML data into the file QTextStream in_stream(&index_file); { @@ -200,6 +211,7 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-version-number", mod.version_number.toStdString() }, + { "x-prismlauncher-dependencies", deps }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -330,6 +342,21 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod return {}; } } + { // dependencies + auto deps = table["x-prismlauncher-dependencies"].as_array(); + if (deps) { + for (auto&& depNode : *deps) { + auto dep = depNode.as_table(); + if (dep) { + ModPlatform::Dependency d; + d.addonId = stringEntry(*dep, "addonId"); + d.version = stringEntry(*dep, "version"); + d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type")); + mod.dependencies << d; + } + } + } + } return mod; } diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index ba9a0fe75..b5b8894f3 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -55,6 +55,8 @@ class V1 { QVariant project_id{}; QString version_number{}; + QList dependencies; + public: // This is a totally heuristic, but should work for now. auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } From 22539a4ff8d7e6c7a28e63be30ee7e9ee3888378 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 3 May 2025 13:27:40 +0300 Subject: [PATCH 03/18] feat: add requireBy and requires columns Signed-off-by: Trial97 --- launcher/minecraft/mod/Mod.cpp | 31 +++++++ launcher/minecraft/mod/Mod.h | 9 +++ launcher/minecraft/mod/ModFolderModel.cpp | 98 ++++++++++++++++++++--- launcher/minecraft/mod/ModFolderModel.h | 9 +++ launcher/minecraft/mod/Resource.h | 16 +++- 5 files changed, 153 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index fd3b5cb46..661192d67 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -105,6 +105,20 @@ int Mod::compare(const Resource& other, SortType type) const return compare_result; break; } + case SortType::REQUIRED_BY: { + if (requiredByCount() > cast_other->requiredByCount()) + return 1; + if (requiredByCount() < cast_other->requiredByCount()) + return -1; + break; + } + case SortType::REQUIRES: { + if (requiresCount() > cast_other->requiresCount()) + return 1; + if (requiresCount() < cast_other->requiresCount()) + return -1; + break; + } } return 0; } @@ -288,3 +302,20 @@ QStringList Mod::dependencies() const { return details().dependencies; } + +int Mod::requiredByCount() const +{ + return m_requiredByCount; +} +int Mod::requiresCount() const +{ + return m_requiresCount; +} +void Mod::setRequiredByCount(int value) +{ + m_requiredByCount = value; +} +void Mod::setRequiresCount(int value) +{ + m_requiresCount = value; +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 7bac7d0f4..0d24409bf 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -72,6 +72,12 @@ class Mod : public Resource { auto releaseType() const -> QString; QStringList dependencies() const; + int requiredByCount() const; + int requiresCount() const; + + void setRequiredByCount(int value); + void setRequiresCount(int value); + /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ @@ -103,4 +109,7 @@ class Mod : public Resource { bool wasEverUsed = false; bool wasReadAttempt = false; } mutable m_packImageCacheKey; + + int m_requiredByCount = 0; + int m_requiresCount = 0; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 45ec76f19..d810b3428 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -48,23 +48,29 @@ #include #include #include +#include #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "modplatform/ModIndex.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", - "Minecraft Versions", "Release Type" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), - tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, - SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, - SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; + "Minecraft Versions", "Release Type", "Requires", "Required by" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), + tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires "), tr("Required by") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, + SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, + SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true }; + + connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished); } QVariant ModFolderModel::data(const QModelIndex& index, int role) const @@ -108,8 +114,15 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case ReleaseTypeColumn: { return at(row).releaseType(); } - case SizeColumn: + case SizeColumn: { return at(row).sizeStr(); + } + case RequiredByColumn: { + return at(row).requiredByCount(); + } + case RequiresColumn: { + return at(row).requiresCount(); + } default: return QVariant(); } @@ -166,6 +179,8 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case McVersionsColumn: case ReleaseTypeColumn: case SizeColumn: + case RequiredByColumn: + case RequiresColumn: return columnNames().at(section); default: return QVariant(); @@ -193,6 +208,10 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio return tr("The release type."); case SizeColumn: return tr("The size of the mod."); + case RequiredByColumn: + return tr("Number of mods for what this is needed."); + case RequiresColumn: + return tr("Number of mods that this requires."); default: return QVariant(); } @@ -238,3 +257,64 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } + +void ModFolderModel::onParseFinished() +{ + if (hasPendingParseTasks()) { + return; + } + auto mods = allMods(); + + auto findById = [mods](QString modId) -> Mod* { + auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); + return found != mods.end() ? *found : nullptr; + }; + auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { + auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { + return m->metadata()->provider == provider && m->metadata()->project_id == modId; + }); + return found != mods.end() ? *found : nullptr; + }; + for (auto mod : mods) { + auto id = mod->internal_id(); + for (auto dep : mod->dependencies()) { + auto d = findById(dep); + if (d) { + m_requires[id] << d; + m_requiredBy[d->internal_id()] << mod; + } + } + for (auto dep : mod->metadata()->dependencies) { + auto d = findByProjectID(dep.addonId, mod->metadata()->provider); + if (d) { + m_requires[id] << d; + m_requiredBy[d->internal_id()] << mod; + } + } + } + auto removeDuplicates = [](QList& list) { + std::set seen; + auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { + auto id = m->internal_id(); + if (seen.count(id) > 0) { + return true; + } + seen.insert(id); + return false; + }); + list.erase(it, list.end()); + }; + for (auto key : m_requiredBy.keys()) { + removeDuplicates(m_requiredBy[key]); + } + for (auto key : m_requires.keys()) { + removeDuplicates(m_requires[key]); + } + for (auto mod : mods) { + auto id = mod->internal_id(); + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 42868dc91..40f9eb733 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/mod/Resource.h" class BaseInstance; class QFileSystemWatcher; @@ -69,6 +71,8 @@ class ModFolderModel : public ResourceFolderModel { LoadersColumn, McVersionsColumn, ReleaseTypeColumn, + RequiresColumn, + RequiredByColumn, NUM_COLUMNS }; ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); @@ -89,4 +93,9 @@ class ModFolderModel : public ResourceFolderModel { private slots: void onParseSucceeded(int ticket, QString resource_id) override; + void onParseFinished(); + + private: + QHash> m_requiredBy; + QHash> m_requires; }; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 242b8a30d..3eda4c013 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -58,7 +58,21 @@ enum class ResourceStatus { UNKNOWN, // Default status }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; +enum class SortType { + NAME, + DATE, + VERSION, + ENABLED, + PACK_FORMAT, + PROVIDER, + SIZE, + SIDE, + MC_VERSIONS, + LOADERS, + RELEASE_TYPE, + REQUIRES, + REQUIRED_BY, +}; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; From 66f0f95fd74df852023f203dd1f0e744be6403ab Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 5 May 2025 00:35:35 +0300 Subject: [PATCH 04/18] feat: make dependencies auto disable/enable Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 97 ++++++++++++++++--- launcher/minecraft/mod/ModFolderModel.h | 4 + .../minecraft/mod/ResourceFolderModel.cpp | 1 + launcher/ui/pages/instance/ModFolderPage.cpp | 20 +++- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d810b3428..e65759eec 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -50,6 +50,9 @@ #include #include +#include "minecraft/Component.h" +#include "minecraft/mod/Resource.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" @@ -258,6 +261,12 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } +Mod* findById(QList mods, QString modId) +{ + auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); + return found != mods.end() ? *found : nullptr; +} + void ModFolderModel::onParseFinished() { if (hasPendingParseTasks()) { @@ -265,37 +274,37 @@ void ModFolderModel::onParseFinished() } auto mods = allMods(); - auto findById = [mods](QString modId) -> Mod* { - auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); - return found != mods.end() ? *found : nullptr; - }; auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { - return m->metadata()->provider == provider && m->metadata()->project_id == modId; + return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; }); return found != mods.end() ? *found : nullptr; }; for (auto mod : mods) { - auto id = mod->internal_id(); + auto id = mod->mod_id(); for (auto dep : mod->dependencies()) { - auto d = findById(dep); + auto d = findById(mods, dep); if (d) { m_requires[id] << d; - m_requiredBy[d->internal_id()] << mod; + m_requiredBy[d->mod_id()] << mod; } } - for (auto dep : mod->metadata()->dependencies) { - auto d = findByProjectID(dep.addonId, mod->metadata()->provider); - if (d) { - m_requires[id] << d; - m_requiredBy[d->internal_id()] << mod; + if (mod->metadata()) { + for (auto dep : mod->metadata()->dependencies) { + if (dep.type == ModPlatform::DependencyType::REQUIRED) { + auto d = findByProjectID(dep.addonId, mod->metadata()->provider); + if (d) { + m_requires[id] << d; + m_requiredBy[d->mod_id()] << mod; + } + } } } } auto removeDuplicates = [](QList& list) { std::set seen; auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { - auto id = m->internal_id(); + auto id = m->mod_id(); if (seen.count(id) > 0) { return true; } @@ -311,10 +320,66 @@ void ModFolderModel::onParseFinished() removeDuplicates(m_requires[key]); } for (auto mod : mods) { - auto id = mod->internal_id(); + auto id = mod->mod_id(); mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiresCount(m_requires[id].count()); - int row = m_resources_index[id]; + int row = m_resources_index[mod->internal_id()]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } } + +QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto indexedMods = selectedMods(indexes); + if (action == EnableAction::TOGGLE) { + if (indexedMods.length() != 1) { + return {}; // not sure how to handle a bunch of rows that are toggled(not even sure it is posible) + } + action = indexedMods.first()->enabled() ? EnableAction::DISABLE : EnableAction::ENABLE; + } + + std::set seen; + bool shouldBeEnabled = action == EnableAction::ENABLE; + for (auto mod : indexedMods) { + auto id = mod->mod_id(); + QList mods; + switch (action) { + case EnableAction::DISABLE: { + mods = m_requiredBy[id]; + break; + } + case EnableAction::ENABLE: { + mods = m_requires[id]; + break; + } + case EnableAction::TOGGLE: + break; + } + for (auto affected : mods) { + auto affectedId = affected->mod_id(); + + if (findById(indexedMods, affectedId) == nullptr && seen.count(affectedId) == 0) { + seen.insert(affectedId); + if (shouldBeEnabled != affected->enabled()) { + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); + } + } + } + } + // collect the affected mods until all of them are included in the list + if (!affectedList.isEmpty()) { + affectedList += getAffectedMods(indexes + affectedList, action); + } + return affectedList; +} + +bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + auto affected = getAffectedMods(indexes, action); + return ResourceFolderModel::setResourceEnabled(indexes + affected, action); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 40f9eb733..ec1c60daa 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -47,6 +47,7 @@ #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/Component.h" #include "minecraft/mod/Resource.h" class BaseInstance; @@ -89,6 +90,9 @@ class ModFolderModel : public ResourceFolderModel { bool isValid(); + bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; + QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); + RESOURCE_HELPERS(Mod) private slots: diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index b0082653d..ff3498882 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -902,6 +902,7 @@ QList ResourceFolderModel::allResources() result.append((resource.get())); return result; } + QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index c39c5ed9d..8ea2b7be8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -37,6 +37,7 @@ */ #include "ModFolderPage.h" +#include "minecraft/mod/Resource.h" #include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_ExternalResourcesPage.h" @@ -91,7 +92,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool()); connect(depsDisabled.get(), &Setting::SettingChanged, this, - [this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); + [this](const Setting&, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); @@ -136,7 +137,22 @@ void ModFolderPage::removeItems(const QItemSelection& selection) if (response != QMessageBox::Yes) return; } - m_model->deleteResources(selection.indexes()); + + auto indexes = selection.indexes(); + auto affected = m_model->getAffectedMods(indexes, EnableAction::DISABLE); + if (!affected.isEmpty()) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Disable"), + tr("The mods you are tring to disable are required by %1 mods.\n" + "Do you want to disable them?") + .arg(affected.length()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) { + m_model->setResourceEnabled(affected, EnableAction::DISABLE); + } + } + m_model->deleteResources(indexes); } void ModFolderPage::downloadMods() From 947656df0f03763976d3261668efb2fbe62dcca5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 28 Jun 2025 23:50:54 +0300 Subject: [PATCH 05/18] feat: display mod dependencies Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 17 ++ launcher/minecraft/mod/ModFolderModel.h | 4 + launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/widgets/InfoFrame.cpp | 107 +++++--- launcher/ui/widgets/InfoFrame.h | 4 +- launcher/ui/widgets/InfoFrame.ui | 258 ++++++++++++------- 6 files changed, 256 insertions(+), 136 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e65759eec..e36e0f10f 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -383,3 +383,20 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc auto affected = getAffectedMods(indexes, action); return ResourceFolderModel::setResourceEnabled(indexes + affected, action); } + +QStringList reqToList(QList l) +{ + QStringList req; + for (auto m : l) { + req << m->name(); + } + return req; +} +QStringList ModFolderModel::requiresList(QString id) +{ + return reqToList(m_requires[id]); +} +QStringList ModFolderModel::requiredByList(QString id) +{ + return reqToList(m_requiredBy[id]); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index ec1c60daa..e794cacdc 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -95,6 +95,10 @@ class ModFolderModel : public ResourceFolderModel { RESOURCE_HELPERS(Mod) + public: + QStringList requiresList(QString id); + QStringList requiredByList(QString id); + private slots: void onParseSucceeded(int ticket, QString resource_id) override; void onParseFinished(); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8ea2b7be8..dbfabdec3 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -122,7 +122,7 @@ void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] con auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); const Mod& mod = m_model->at(row); - ui->frame->updateWithMod(mod); + ui->frame->updateWithMod(mod, m_model->requiresList(mod.mod_id()), m_model->requiredByList(mod.mod_id())); } void ModFolderPage::removeItems(const QItemSelection& selection) diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 1e641c4f9..cab2889ac 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -54,28 +54,34 @@ void setupLinkToolTip(QLabel* label) }); } -InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame) +InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), m_ui(new Ui::InfoFrame) { - ui->setupUi(this); - ui->descriptionLabel->setHidden(true); - ui->nameLabel->setHidden(true); - ui->licenseLabel->setHidden(true); - ui->issueTrackerLabel->setHidden(true); + m_ui->setupUi(this); + m_ui->descriptionLabel->setHidden(true); + m_ui->nameLabel->setHidden(true); + m_ui->licenseLabel->setHidden(true); + m_ui->issueTrackerLabel->setHidden(true); - setupLinkToolTip(ui->iconLabel); - setupLinkToolTip(ui->descriptionLabel); - setupLinkToolTip(ui->nameLabel); - setupLinkToolTip(ui->licenseLabel); - setupLinkToolTip(ui->issueTrackerLabel); + setupLinkToolTip(m_ui->iconLabel); + setupLinkToolTip(m_ui->descriptionLabel); + setupLinkToolTip(m_ui->nameLabel); + setupLinkToolTip(m_ui->licenseLabel); + setupLinkToolTip(m_ui->issueTrackerLabel); updateHiddenState(); + connect(m_ui->moreInfoBtn, &QPushButton::clicked, this, [this]() { + auto nextIndex = (m_ui->infoStacked->currentIndex() + 1) % 2; + m_ui->infoStacked->setCurrentIndex(nextIndex); + m_ui->moreInfoBtn->setText(nextIndex == 0 ? ">" : "<"); + }); + m_ui->moreInfoBtn->hide(); } InfoFrame::~InfoFrame() { - delete ui; + delete m_ui; } -void InfoFrame::updateWithMod(Mod const& m) +void InfoFrame::updateWithMod(Mod const& m, QStringList requiresList, QStringList requiredByList) { if (m.type() == ResourceType::FOLDER) { clear(); @@ -141,6 +147,26 @@ void InfoFrame::updateWithMod(Mod const& m) issueTracker += "" + m.issueTracker() + ""; } setIssueTracker(issueTracker); + if (requiredByList.isEmpty()) { + m_ui->requiredGB->hide(); + } else { + m_ui->requiredGB->show(); + m_ui->requiredView->clear(); + m_ui->requiredView->addItems(requiredByList); + } + + if (requiresList.isEmpty()) { + m_ui->requiresGB->hide(); + } else { + m_ui->requiresGB->show(); + m_ui->requiresView->clear(); + m_ui->requiresView->addItems(requiresList); + } + if (requiresList.isEmpty() && requiredByList.isEmpty()) { + m_ui->infoStacked->setCurrentIndex(0); + m_ui->moreInfoBtn->setText(">"); + } + m_ui->moreInfoBtn->setHidden(requiresList.isEmpty() && requiredByList.isEmpty()); } void InfoFrame::updateWithResource(const Resource& resource) @@ -255,12 +281,13 @@ void InfoFrame::clear() setImage(); setLicense(); setIssueTracker(); + m_ui->moreInfoBtn->hide(); } void InfoFrame::updateHiddenState() { - if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && - ui->issueTrackerLabel->isHidden()) { + if (m_ui->descriptionLabel->isHidden() && m_ui->nameLabel->isHidden() && m_ui->licenseLabel->isHidden() && + m_ui->issueTrackerLabel->isHidden()) { setHidden(true); } else { setHidden(false); @@ -270,10 +297,10 @@ void InfoFrame::updateHiddenState() void InfoFrame::setName(QString text) { if (text.isEmpty()) { - ui->nameLabel->setHidden(true); + m_ui->nameLabel->setHidden(true); } else { - ui->nameLabel->setText(text); - ui->nameLabel->setHidden(false); + m_ui->nameLabel->setText(text); + m_ui->nameLabel->setHidden(false); } updateHiddenState(); } @@ -281,14 +308,14 @@ void InfoFrame::setName(QString text) void InfoFrame::setDescription(QString text) { if (text.isEmpty()) { - ui->descriptionLabel->setHidden(true); + m_ui->descriptionLabel->setHidden(true); updateHiddenState(); return; } else { - ui->descriptionLabel->setHidden(false); + m_ui->descriptionLabel->setHidden(false); updateHiddenState(); } - ui->descriptionLabel->setToolTip(""); + m_ui->descriptionLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -310,8 +337,8 @@ void InfoFrame::setDescription(QString text) doc.setHtml(text); if (doc.characterCount() > maxCharacterElide) { - ui->descriptionLabel->setOpenExternalLinks(false); - ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. + m_ui->descriptionLabel->setOpenExternalLinks(false); + m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; // move the cursor to the character elide, doesn't see html @@ -324,25 +351,25 @@ void InfoFrame::setDescription(QString text) cursor.insertHtml("..."); labeltext.append(doc.toHtml()); - connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + connect(m_ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { - ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); + m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - ui->descriptionLabel->setText(labeltext); + m_ui->descriptionLabel->setText(labeltext); } void InfoFrame::setLicense(QString text) { if (text.isEmpty()) { - ui->licenseLabel->setHidden(true); + m_ui->licenseLabel->setHidden(true); updateHiddenState(); return; } else { - ui->licenseLabel->setHidden(false); + m_ui->licenseLabel->setHidden(false); updateHiddenState(); } - ui->licenseLabel->setToolTip(""); + m_ui->licenseLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -358,26 +385,26 @@ void InfoFrame::setLicense(QString text) QString labeltext; labeltext.reserve(300); if (finaltext.length() > 290) { - ui->licenseLabel->setOpenExternalLinks(false); - ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); + m_ui->licenseLabel->setOpenExternalLinks(false); + m_ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); - connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + connect(m_ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { - ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); + m_ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - ui->licenseLabel->setText(labeltext); + m_ui->licenseLabel->setText(labeltext); } void InfoFrame::setIssueTracker(QString text) { if (text.isEmpty()) { - ui->issueTrackerLabel->setHidden(true); + m_ui->issueTrackerLabel->setHidden(true); } else { - ui->issueTrackerLabel->setText(text); - ui->issueTrackerLabel->setHidden(false); + m_ui->issueTrackerLabel->setText(text); + m_ui->issueTrackerLabel->setHidden(false); } updateHiddenState(); } @@ -385,10 +412,10 @@ void InfoFrame::setIssueTracker(QString text) void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { - ui->iconLabel->setHidden(true); + m_ui->iconLabel->setHidden(true); } else { - ui->iconLabel->setHidden(false); - ui->iconLabel->setPixmap(img); + m_ui->iconLabel->setHidden(false); + m_ui->iconLabel->setPixmap(img); } } diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 20c54e2e5..9cec3d2f2 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -61,7 +61,7 @@ class InfoFrame : public QFrame { void clear(); - void updateWithMod(Mod const& m); + void updateWithMod(Mod const& m, QStringList requiresList = {}, QStringList requiredByList = {}); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); void updateWithDataPack(DataPack& rp); @@ -78,7 +78,7 @@ class InfoFrame : public QFrame { void updateHiddenState(); private: - Ui::InfoFrame* ui; + Ui::InfoFrame* m_ui; QString m_description; QString m_license; class QMessageBox* m_current_box = nullptr; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index c4d8c83d3..3e044f06f 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -7,7 +7,7 @@ 0 0 527 - 113 + 130 @@ -19,7 +19,7 @@ 16777215 - 120 + 130 @@ -35,6 +35,169 @@ 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 + + + + + + + + + + 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 + + + + + + + + + + + Requires + + + + + + true + + + QListView::Adjust + + + 10 + + + QListView::IconMode + + + + + + + + + + Required by + + + + + + true + + + QListView::Static + + + QListView::Adjust + + + 10 + + + QListView::IconMode + + + + + + + + + + @@ -60,97 +223,6 @@ - - - - - - - - - - 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 - - - - - - - - - - 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 - - - From ba4d9cadc4cc805268ec0dbd89478379213627fd Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 20 Jul 2025 20:10:00 +0300 Subject: [PATCH 06/18] change to QSet to remove duplicates Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 22 ++-------------------- launcher/minecraft/mod/ModFolderModel.h | 5 ++--- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e36e0f10f..80e40fff1 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -301,24 +301,6 @@ void ModFolderModel::onParseFinished() } } } - auto removeDuplicates = [](QList& list) { - std::set seen; - auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { - auto id = m->mod_id(); - if (seen.count(id) > 0) { - return true; - } - seen.insert(id); - return false; - }); - list.erase(it, list.end()); - }; - for (auto key : m_requiredBy.keys()) { - removeDuplicates(m_requiredBy[key]); - } - for (auto key : m_requires.keys()) { - removeDuplicates(m_requires[key]); - } for (auto mod : mods) { auto id = mod->mod_id(); mod->setRequiredByCount(m_requiredBy[id].count()); @@ -346,7 +328,7 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, bool shouldBeEnabled = action == EnableAction::ENABLE; for (auto mod : indexedMods) { auto id = mod->mod_id(); - QList mods; + QSet mods; switch (action) { case EnableAction::DISABLE: { mods = m_requiredBy[id]; @@ -384,7 +366,7 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc return ResourceFolderModel::setResourceEnabled(indexes + affected, action); } -QStringList reqToList(QList l) +QStringList reqToList(QSet l) { QStringList req; for (auto m : l) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index e794cacdc..e47c18405 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -40,7 +40,6 @@ #include #include #include -#include #include #include #include @@ -104,6 +103,6 @@ class ModFolderModel : public ResourceFolderModel { void onParseFinished(); private: - QHash> m_requiredBy; - QHash> m_requires; + QHash> m_requiredBy; + QHash> m_requires; }; From 36e540ed11b337e7561e5750339833ea4fe85454 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 24 Jul 2025 18:42:48 +0300 Subject: [PATCH 07/18] fix toggle action Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 133 +++++++++++++++------- 1 file changed, 94 insertions(+), 39 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 80e40fff1..c706c543a 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -38,6 +38,7 @@ #include "ModFolderModel.h" #include +#include #include #include #include @@ -310,60 +311,114 @@ void ModFolderModel::onParseFinished() } } -QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +QList collectMods(QList mods, QHash> relation, std::set& seen) { - if (indexes.isEmpty()) - return {}; - - QModelIndexList affectedList = {}; - auto indexedMods = selectedMods(indexes); - if (action == EnableAction::TOGGLE) { - if (indexedMods.length() != 1) { - return {}; // not sure how to handle a bunch of rows that are toggled(not even sure it is posible) - } - action = indexedMods.first()->enabled() ? EnableAction::DISABLE : EnableAction::ENABLE; - } - - std::set seen; - bool shouldBeEnabled = action == EnableAction::ENABLE; - for (auto mod : indexedMods) { + QList affectedList = {}; + for (auto mod : mods) { auto id = mod->mod_id(); - QSet mods; - switch (action) { - case EnableAction::DISABLE: { - mods = m_requiredBy[id]; - break; - } - case EnableAction::ENABLE: { - mods = m_requires[id]; - break; - } - case EnableAction::TOGGLE: - break; - } - for (auto affected : mods) { - auto affectedId = affected->mod_id(); + if (seen.count(id) == 0) { + seen.insert(id); + for (auto affected : relation[id]) { + auto affectedId = affected->mod_id(); - if (findById(indexedMods, affectedId) == nullptr && seen.count(affectedId) == 0) { - seen.insert(affectedId); - if (shouldBeEnabled != affected->enabled()) { - auto row = m_resources_index[affected->internal_id()]; - affectedList << index(row, 0); + if (findById(mods, affectedId) == nullptr && seen.count(affectedId) == 0) { + seen.insert(affectedId); + affectedList << affected; } } } } // collect the affected mods until all of them are included in the list if (!affectedList.isEmpty()) { - affectedList += getAffectedMods(indexes + affectedList, action); + affectedList += collectMods(affectedList, relation, seen); + } + return affectedList; +} + +QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto affectedMods = selectedMods(indexes); + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + affectedMods << collectMods(affectedMods, m_requires, seen); + break; + } + case EnableAction::DISABLE: { + affectedMods << collectMods(affectedMods, m_requiredBy, seen); + break; + } + case EnableAction::TOGGLE: { + return {}; // this function should not be called with TOGGLE + } + } + bool shouldBeEnabled = action == EnableAction::ENABLE; + for (auto affected : affectedMods) { + auto affectedId = affected->mod_id(); + if (shouldBeEnabled != affected->enabled()) { + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); + } } return affectedList; } bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { - auto affected = getAffectedMods(indexes, action); - return ResourceFolderModel::setResourceEnabled(indexes + affected, action); + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto indexedMods = selectedMods(indexes); + + QList toEnable = {}; + QList toDisable = {}; + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + toEnable = indexedMods; + break; + } + case EnableAction::DISABLE: { + toDisable = indexedMods; + break; + } + case EnableAction::TOGGLE: { + for (auto mod : indexedMods) { + if (mod->enabled()) { + toDisable << mod; + } else { + toEnable << mod; + } + } + break; + } + } + + toEnable << collectMods(toEnable, m_requires, seen); + toDisable << collectMods(toDisable, m_requiredBy, seen); + + toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); + auto toList = [this](QList mods, bool shouldBeEnabled) { + QModelIndexList list; + for (auto mod : mods) { + if (shouldBeEnabled != mod->enabled()) { + auto row = m_resources_index[mod->internal_id()]; + list << index(row, 0); + } + } + return list; + }; + + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable, false), EnableAction::DISABLE); + auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable, true), EnableAction::ENABLE); + return disableStatus && enableStatus; } QStringList reqToList(QSet l) From 1a82a83e36602e67cb730674e5e82d47a43b0c0e Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Jul 2025 17:50:29 +0300 Subject: [PATCH 08/18] rework parse dependency function Signed-off-by: Trial97 --- .../minecraft/mod/tasks/LocalModParseTask.cpp | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index c82392117..7d76317f2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -82,11 +82,11 @@ ModDetails ReadMCModInfo(QByteArray contents) }; if (firstObj.contains("requiredMods")) { - for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + for (auto dep : firstObj.value("requiredMods").toArray()) { addDep(dep.toString()); } } else if (firstObj.contains("dependencies")) { - for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + for (auto dep : firstObj.value("dependencies").toArray()) { addDep(dep.toString()); } } @@ -229,19 +229,29 @@ ModDetails ReadMCModTOML(QByteArray contents) details.icon_file = logoFile; auto parseDep = [&details](toml::array* dependencies) { - if (dependencies) { - for (auto& dep : *dependencies) { - auto dep_table = dep.as_table(); - if (dep_table) { - auto modId = dep_table->get("modId")->value_or(""); - if (modId != "forge" && modId != "neoforge" && modId != "minecraft") { - if (dep_table->contains("type") && (dep_table->get("type"))->value_or("") == "required") { - details.dependencies.append(QString::fromStdString(modId)); - } else if (dep_table->contains("mandatory") && (dep_table->get("mandatory"))->value_or(false)) { - details.dependencies.append(QString::fromStdString(modId)); - } - } - } + static const QStringList ignoreModIds = { "", "forge", "neoforge", "minecraft" }; + if (!dependencies) { + return; + } + auto isNeoForgeDep = [](toml::table* t) { + auto type = (*t)["type"].as_string(); + return type && type->get() == "required"; + }; + auto isForgeDep = [](toml::table* t) { + auto mandatory = (*t)["mandatory"].as_boolean(); + return mandatory && mandatory->get(); + }; + for (auto& dep : *dependencies) { + auto dep_table = dep.as_table(); + if (!dep_table) { + continue; + } + auto modId = (*dep_table)["modId"].as_string(); + if (!modId || ignoreModIds.contains(modId->get())) { + continue; + } + if (isNeoForgeDep(dep_table) || isForgeDep(dep_table)) { + details.dependencies.append(QString::fromStdString(modId->get())); } } }; From c9426da64def7c1ab600818e5a1c48c17e08ea9f Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 13 Sep 2025 21:41:14 +0300 Subject: [PATCH 09/18] Update launcher/minecraft/mod/ModFolderModel.cpp Co-authored-by: TheKodeToad Signed-off-by: Alexandru Ionut Tripon Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index c706c543a..6e6132296 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -61,10 +61,10 @@ ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_ : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", - "Minecraft Versions", "Release Type", "Requires", "Required by" }); + "Minecraft Versions", "Release Type", "Requires", "Required By" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), - tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires "), tr("Required by") }); + tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires"), tr("Required By") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY }; From 8c824ed3d64f92e48ff3ae50688bacddc4d4a67b Mon Sep 17 00:00:00 2001 From: Alexandru Ionut Tripon Date: Tue, 5 Aug 2025 23:30:30 +0300 Subject: [PATCH 10/18] Update launcher/minecraft/mod/ModFolderModel.cpp Co-authored-by: TheKodeToad Signed-off-by: Alexandru Ionut Tripon Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 6e6132296..72ab66e61 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -213,9 +213,9 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case SizeColumn: return tr("The size of the mod."); case RequiredByColumn: - return tr("Number of mods for what this is needed."); + return tr("For each mod, the number of other mods which depend on it."); case RequiresColumn: - return tr("Number of mods that this requires."); + return tr("For each mod, the number of other mods it depends on."); default: return QVariant(); } From 1735814d423355b8d8a3e1448a15e36089826370 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 20 Oct 2025 01:01:10 +0300 Subject: [PATCH 11/18] add confirmation dialog Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 82 +++++++++++++++++++---- launcher/minecraft/mod/ModFolderModel.h | 2 + 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 72ab66e61..2bcbf1fcb 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -38,8 +38,9 @@ #include "ModFolderModel.h" #include -#include +#include #include +#include #include #include #include @@ -56,6 +57,7 @@ #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) @@ -262,7 +264,7 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } -Mod* findById(QList mods, QString modId) +Mod* findById(QSet mods, QString modId) { auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); return found != mods.end() ? *found : nullptr; @@ -273,7 +275,8 @@ void ModFolderModel::onParseFinished() if (hasPendingParseTasks()) { return; } - auto mods = allMods(); + auto modsList = allMods(); + auto mods = QSet(modsList.begin(), modsList.end()); auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { @@ -311,9 +314,9 @@ void ModFolderModel::onParseFinished() } } -QList collectMods(QList mods, QHash> relation, std::set& seen) +QSet collectMods(QSet mods, QHash> relation, std::set& seen) { - QList affectedList = {}; + QSet affectedList = {}; for (auto mod : mods) { auto id = mod->mod_id(); if (seen.count(id) == 0) { @@ -341,16 +344,17 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, return {}; QModelIndexList affectedList = {}; - auto affectedMods = selectedMods(indexes); + auto affectedModsList = selectedMods(indexes); + auto affectedMods = QSet(affectedModsList.begin(), affectedModsList.end()); std::set seen; switch (action) { case EnableAction::ENABLE: { - affectedMods << collectMods(affectedMods, m_requires, seen); + affectedMods = collectMods(affectedMods, m_requires, seen); break; } case EnableAction::DISABLE: { - affectedMods << collectMods(affectedMods, m_requiredBy, seen); + affectedMods = collectMods(affectedMods, m_requiredBy, seen); break; } case EnableAction::TOGGLE: { @@ -374,10 +378,11 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc return {}; QModelIndexList affectedList = {}; - auto indexedMods = selectedMods(indexes); + auto indexedModsList = selectedMods(indexes); + auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); - QList toEnable = {}; - QList toDisable = {}; + QSet toEnable = {}; + QSet toDisable = {}; std::set seen; switch (action) { @@ -401,11 +406,11 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc } } - toEnable << collectMods(toEnable, m_requires, seen); - toDisable << collectMods(toDisable, m_requiredBy, seen); + auto requiredToEnable = collectMods(toEnable, m_requires, seen); + auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen); toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); - auto toList = [this](QList mods, bool shouldBeEnabled) { + auto toList = [this](QSet mods, bool shouldBeEnabled) { QModelIndexList list; for (auto mod : mods) { if (shouldBeEnabled != mod->enabled()) { @@ -416,6 +421,26 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc return list; }; + if (requiredToEnable.size() > 0 || requiredToDisable.size() > 0) { + auto box = CustomMessageBox::selectable( + nullptr, tr("Confirm toggle"), + tr("Toggling this mod(s) will cause changes to other mods.\n") + + (requiredToEnable.size() > 0 ? tr("%1 mod(s) will be enabled\n").arg(requiredToEnable.size()) : "") + + (requiredToDisable.size() > 0 ? tr("%1 mod(s) will be disabled\n").arg(requiredToDisable.size()) : "") + + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); + auto b = box->button(QMessageBox::No); + b->setText(tr("No, just toggle selected")); + auto response = box->exec(); + + if (response == QMessageBox::Yes) { + toEnable |= requiredToEnable; + toDisable |= requiredToDisable; + } else if (response == QMessageBox::Cancel) { + return false; + } + } + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable, false), EnableAction::DISABLE); auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable, true), EnableAction::ENABLE); return disableStatus && enableStatus; @@ -429,11 +454,40 @@ QStringList reqToList(QSet l) } return req; } + QStringList ModFolderModel::requiresList(QString id) { return reqToList(m_requires[id]); } + QStringList ModFolderModel::requiredByList(QString id) { return reqToList(m_requiredBy[id]); } + +bool ModFolderModel::deleteResources(const QModelIndexList& indexes) +{ + auto deleteInvalid = [](QSet& mods) { + for (auto it = mods.begin(); it != mods.end();) { + auto mod = *it; + // the QFileInfo::exists is used instead of mod->fileinfo().exists + // because the later somehow caches that the file exists + if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) { + it = mods.erase(it); + } else { + ++it; + } + } + }; + auto rsp = ResourceFolderModel::deleteResources(indexes); + for (auto mod : allMods()) { + auto id = mod->mod_id(); + deleteInvalid(m_requiredBy[id]); + deleteInvalid(m_requires[id]); + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[mod->internal_id()]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } + return rsp; +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index e47c18405..4de875abc 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -90,6 +90,8 @@ class ModFolderModel : public ResourceFolderModel { bool isValid(); bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; + bool deleteResources(const QModelIndexList& indexes) override; + QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); RESOURCE_HELPERS(Mod) From 35750ebada9dffdd63176e91ae8198bd6383c967 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 19 Nov 2025 22:50:24 +0200 Subject: [PATCH 12/18] update toggle message box Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 82 +++--- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/widgets/InfoFrame.cpp | 110 +++----- launcher/ui/widgets/InfoFrame.h | 4 +- launcher/ui/widgets/InfoFrame.ui | 258 +++++++------------ 5 files changed, 189 insertions(+), 267 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 2bcbf1fcb..2c15f30f4 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -278,6 +278,9 @@ void ModFolderModel::onParseFinished() auto modsList = allMods(); auto mods = QSet(modsList.begin(), modsList.end()); + m_requires.clear(); + m_requiredBy.clear(); + auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; @@ -314,9 +317,10 @@ void ModFolderModel::onParseFinished() } } -QSet collectMods(QSet mods, QHash> relation, std::set& seen) +QSet collectMods(QSet mods, QHash> relation, std::set& seen, bool shouldBeEnabled) { QSet affectedList = {}; + QSet needToCheck = {}; for (auto mod : mods) { auto id = mod->mod_id(); if (seen.count(id) == 0) { @@ -326,14 +330,17 @@ QSet collectMods(QSet mods, QHash> relation, std if (findById(mods, affectedId) == nullptr && seen.count(affectedId) == 0) { seen.insert(affectedId); - affectedList << affected; + if (shouldBeEnabled != affected->enabled()) { + affectedList << affected; + } + needToCheck << affected; } } } } // collect the affected mods until all of them are included in the list - if (!affectedList.isEmpty()) { - affectedList += collectMods(affectedList, relation, seen); + if (!needToCheck.isEmpty()) { + affectedList += collectMods(needToCheck, relation, seen, shouldBeEnabled); } return affectedList; } @@ -350,24 +357,21 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, switch (action) { case EnableAction::ENABLE: { - affectedMods = collectMods(affectedMods, m_requires, seen); + affectedMods = collectMods(affectedMods, m_requires, seen, true); break; } case EnableAction::DISABLE: { - affectedMods = collectMods(affectedMods, m_requiredBy, seen); + affectedMods = collectMods(affectedMods, m_requiredBy, seen, false); break; } case EnableAction::TOGGLE: { return {}; // this function should not be called with TOGGLE } } - bool shouldBeEnabled = action == EnableAction::ENABLE; for (auto affected : affectedMods) { auto affectedId = affected->mod_id(); - if (shouldBeEnabled != affected->enabled()) { - auto row = m_resources_index[affected->internal_id()]; - affectedList << index(row, 0); - } + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); } return affectedList; } @@ -377,7 +381,6 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc if (indexes.isEmpty()) return {}; - QModelIndexList affectedList = {}; auto indexedModsList = selectedMods(indexes); auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); @@ -406,31 +409,50 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc } } - auto requiredToEnable = collectMods(toEnable, m_requires, seen); - auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen); + auto requiredToEnable = collectMods(toEnable, m_requires, seen, true); + auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false); toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); - auto toList = [this](QSet mods, bool shouldBeEnabled) { + auto toList = [this](QSet mods) { QModelIndexList list; for (auto mod : mods) { - if (shouldBeEnabled != mod->enabled()) { - auto row = m_resources_index[mod->internal_id()]; - list << index(row, 0); - } + auto row = m_resources_index[mod->internal_id()]; + list << index(row, 0); } return list; }; if (requiredToEnable.size() > 0 || requiredToDisable.size() > 0) { - auto box = CustomMessageBox::selectable( - nullptr, tr("Confirm toggle"), - tr("Toggling this mod(s) will cause changes to other mods.\n") + - (requiredToEnable.size() > 0 ? tr("%1 mod(s) will be enabled\n").arg(requiredToEnable.size()) : "") + - (requiredToDisable.size() > 0 ? tr("%1 mod(s) will be disabled\n").arg(requiredToDisable.size()) : "") + - tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); - auto b = box->button(QMessageBox::No); - b->setText(tr("No, just toggle selected")); + QString title; + QString message; + QString noButton; + QString yesButton; + if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { + title = tr("Confirm toggle"); + message = tr("Toggling this mod(s) will cause changes to other mods.\n") + + tr("%n mod(s) will be enabled\n").arg(requiredToEnable.size()) + + tr("%n mod(s) will be disabled\n").arg(requiredToDisable.size()) + + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); + noButton = tr("Only Toggle Selected"); + yesButton = tr("Toggle Required Mods"); + } else if (requiredToEnable.size() > 0) { + title = tr("Confirm enable"); + message = tr("The enabled mod(s) require %n additional mod(s)\n").arg(requiredToEnable.size()) + + tr("Would you like to enable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Enable Selected"); + yesButton = tr("Enable Required"); + } else { + title = tr("Confirm disable"); + message = tr("The disabled mod(s) are required by %n additional mod(s)\n").arg(requiredToDisable.size()) + + tr("Would you like to disable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Disable Selected"); + yesButton = tr("Disable Required"); + } + + auto box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); + box->button(QMessageBox::No)->setText(noButton); + box->button(QMessageBox::Yes)->setText(yesButton); auto response = box->exec(); if (response == QMessageBox::Yes) { @@ -441,8 +463,8 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc } } - auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable, false), EnableAction::DISABLE); - auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable, true), EnableAction::ENABLE); + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable), EnableAction::DISABLE); + auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable), EnableAction::ENABLE); return disableStatus && enableStatus; } diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index dbfabdec3..8ea2b7be8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -122,7 +122,7 @@ void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] con auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); const Mod& mod = m_model->at(row); - ui->frame->updateWithMod(mod, m_model->requiresList(mod.mod_id()), m_model->requiredByList(mod.mod_id())); + ui->frame->updateWithMod(mod); } void ModFolderPage::removeItems(const QItemSelection& selection) diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index cab2889ac..2363b6592 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -54,34 +54,28 @@ void setupLinkToolTip(QLabel* label) }); } -InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), m_ui(new Ui::InfoFrame) +InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame) { - m_ui->setupUi(this); - m_ui->descriptionLabel->setHidden(true); - m_ui->nameLabel->setHidden(true); - m_ui->licenseLabel->setHidden(true); - m_ui->issueTrackerLabel->setHidden(true); + ui->setupUi(this); + ui->descriptionLabel->setHidden(true); + ui->nameLabel->setHidden(true); + ui->licenseLabel->setHidden(true); + ui->issueTrackerLabel->setHidden(true); - setupLinkToolTip(m_ui->iconLabel); - setupLinkToolTip(m_ui->descriptionLabel); - setupLinkToolTip(m_ui->nameLabel); - setupLinkToolTip(m_ui->licenseLabel); - setupLinkToolTip(m_ui->issueTrackerLabel); + setupLinkToolTip(ui->iconLabel); + setupLinkToolTip(ui->descriptionLabel); + setupLinkToolTip(ui->nameLabel); + setupLinkToolTip(ui->licenseLabel); + setupLinkToolTip(ui->issueTrackerLabel); updateHiddenState(); - connect(m_ui->moreInfoBtn, &QPushButton::clicked, this, [this]() { - auto nextIndex = (m_ui->infoStacked->currentIndex() + 1) % 2; - m_ui->infoStacked->setCurrentIndex(nextIndex); - m_ui->moreInfoBtn->setText(nextIndex == 0 ? ">" : "<"); - }); - m_ui->moreInfoBtn->hide(); } InfoFrame::~InfoFrame() { - delete m_ui; + delete ui; } -void InfoFrame::updateWithMod(Mod const& m, QStringList requiresList, QStringList requiredByList) +void InfoFrame::updateWithMod(Mod const& m) { if (m.type() == ResourceType::FOLDER) { clear(); @@ -147,26 +141,6 @@ void InfoFrame::updateWithMod(Mod const& m, QStringList requiresList, QStringLis issueTracker += "" + m.issueTracker() + ""; } setIssueTracker(issueTracker); - if (requiredByList.isEmpty()) { - m_ui->requiredGB->hide(); - } else { - m_ui->requiredGB->show(); - m_ui->requiredView->clear(); - m_ui->requiredView->addItems(requiredByList); - } - - if (requiresList.isEmpty()) { - m_ui->requiresGB->hide(); - } else { - m_ui->requiresGB->show(); - m_ui->requiresView->clear(); - m_ui->requiresView->addItems(requiresList); - } - if (requiresList.isEmpty() && requiredByList.isEmpty()) { - m_ui->infoStacked->setCurrentIndex(0); - m_ui->moreInfoBtn->setText(">"); - } - m_ui->moreInfoBtn->setHidden(requiresList.isEmpty() && requiredByList.isEmpty()); } void InfoFrame::updateWithResource(const Resource& resource) @@ -253,8 +227,7 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) setImage(resource_pack.image({ 64, 64 })); } -void InfoFrame::updateWithDataPack(DataPack& data_pack) -{ +void InfoFrame::updateWithDataPack(DataPack& data_pack) { setName(renderColorCodes(data_pack.name())); setDescription(renderColorCodes(data_pack.description())); setImage(data_pack.image({ 64, 64 })); @@ -281,13 +254,12 @@ void InfoFrame::clear() setImage(); setLicense(); setIssueTracker(); - m_ui->moreInfoBtn->hide(); } void InfoFrame::updateHiddenState() { - if (m_ui->descriptionLabel->isHidden() && m_ui->nameLabel->isHidden() && m_ui->licenseLabel->isHidden() && - m_ui->issueTrackerLabel->isHidden()) { + if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && + ui->issueTrackerLabel->isHidden()) { setHidden(true); } else { setHidden(false); @@ -297,10 +269,10 @@ void InfoFrame::updateHiddenState() void InfoFrame::setName(QString text) { if (text.isEmpty()) { - m_ui->nameLabel->setHidden(true); + ui->nameLabel->setHidden(true); } else { - m_ui->nameLabel->setText(text); - m_ui->nameLabel->setHidden(false); + ui->nameLabel->setText(text); + ui->nameLabel->setHidden(false); } updateHiddenState(); } @@ -308,14 +280,14 @@ void InfoFrame::setName(QString text) void InfoFrame::setDescription(QString text) { if (text.isEmpty()) { - m_ui->descriptionLabel->setHidden(true); + ui->descriptionLabel->setHidden(true); updateHiddenState(); return; } else { - m_ui->descriptionLabel->setHidden(false); + ui->descriptionLabel->setHidden(false); updateHiddenState(); } - m_ui->descriptionLabel->setToolTip(""); + ui->descriptionLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -337,8 +309,8 @@ void InfoFrame::setDescription(QString text) doc.setHtml(text); if (doc.characterCount() > maxCharacterElide) { - m_ui->descriptionLabel->setOpenExternalLinks(false); - m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. + ui->descriptionLabel->setOpenExternalLinks(false); + ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; // move the cursor to the character elide, doesn't see html @@ -351,25 +323,25 @@ void InfoFrame::setDescription(QString text) cursor.insertHtml("..."); labeltext.append(doc.toHtml()); - connect(m_ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { - m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); + ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - m_ui->descriptionLabel->setText(labeltext); + ui->descriptionLabel->setText(labeltext); } void InfoFrame::setLicense(QString text) { if (text.isEmpty()) { - m_ui->licenseLabel->setHidden(true); + ui->licenseLabel->setHidden(true); updateHiddenState(); return; } else { - m_ui->licenseLabel->setHidden(false); + ui->licenseLabel->setHidden(false); updateHiddenState(); } - m_ui->licenseLabel->setToolTip(""); + ui->licenseLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -385,26 +357,26 @@ void InfoFrame::setLicense(QString text) QString labeltext; labeltext.reserve(300); if (finaltext.length() > 290) { - m_ui->licenseLabel->setOpenExternalLinks(false); - m_ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); + ui->licenseLabel->setOpenExternalLinks(false); + ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); - connect(m_ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { - m_ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); + ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - m_ui->licenseLabel->setText(labeltext); + ui->licenseLabel->setText(labeltext); } void InfoFrame::setIssueTracker(QString text) { if (text.isEmpty()) { - m_ui->issueTrackerLabel->setHidden(true); + ui->issueTrackerLabel->setHidden(true); } else { - m_ui->issueTrackerLabel->setText(text); - m_ui->issueTrackerLabel->setHidden(false); + ui->issueTrackerLabel->setText(text); + ui->issueTrackerLabel->setHidden(false); } updateHiddenState(); } @@ -412,10 +384,10 @@ void InfoFrame::setIssueTracker(QString text) void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { - m_ui->iconLabel->setHidden(true); + ui->iconLabel->setHidden(true); } else { - m_ui->iconLabel->setHidden(false); - m_ui->iconLabel->setPixmap(img); + ui->iconLabel->setHidden(false); + ui->iconLabel->setPixmap(img); } } diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 9cec3d2f2..20c54e2e5 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -61,7 +61,7 @@ class InfoFrame : public QFrame { void clear(); - void updateWithMod(Mod const& m, QStringList requiresList = {}, QStringList requiredByList = {}); + void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); void updateWithDataPack(DataPack& rp); @@ -78,7 +78,7 @@ class InfoFrame : public QFrame { void updateHiddenState(); private: - Ui::InfoFrame* m_ui; + Ui::InfoFrame* ui; QString m_description; QString m_license; class QMessageBox* m_current_box = nullptr; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index 3e044f06f..c4d8c83d3 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -7,7 +7,7 @@ 0 0 527 - 130 + 113 @@ -19,7 +19,7 @@ 16777215 - 130 + 120 @@ -35,169 +35,6 @@ 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 - - - - - - - - - - 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 - - - - - - - - - - - Requires - - - - - - true - - - QListView::Adjust - - - 10 - - - QListView::IconMode - - - - - - - - - - Required by - - - - - - true - - - QListView::Static - - - QListView::Adjust - - - 10 - - - QListView::IconMode - - - - - - - - - - @@ -223,6 +60,97 @@ + + + + + + + + + + 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 + + + + + + + + + + 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 + + + From 67a724f990f5f3432a0ecab7f0948cdf94bb098e Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 19 Nov 2025 23:13:16 +0200 Subject: [PATCH 13/18] improve toggle message Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 2c15f30f4..d711fe0e5 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -430,20 +430,20 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { title = tr("Confirm toggle"); message = tr("Toggling this mod(s) will cause changes to other mods.\n") + - tr("%n mod(s) will be enabled\n").arg(requiredToEnable.size()) + - tr("%n mod(s) will be disabled\n").arg(requiredToDisable.size()) + + tr("%n mod(s) will be enabled\n", "", requiredToEnable.size()) + + tr("%n mod(s) will be disabled\n", "", requiredToDisable.size()) + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); noButton = tr("Only Toggle Selected"); yesButton = tr("Toggle Required Mods"); } else if (requiredToEnable.size() > 0) { title = tr("Confirm enable"); - message = tr("The enabled mod(s) require %n additional mod(s)\n").arg(requiredToEnable.size()) + + message = tr("The enabled mod(s) require %n mod(s).\n", "", requiredToEnable.size()) + tr("Would you like to enable them as well?\nIgnoring them may break the game."); noButton = tr("Only Enable Selected"); yesButton = tr("Enable Required"); } else { title = tr("Confirm disable"); - message = tr("The disabled mod(s) are required by %n additional mod(s)\n").arg(requiredToDisable.size()) + + message = tr("The disabled mod(s) are required by %n mod(s).\n", "", requiredToDisable.size()) + tr("Would you like to disable them as well?\nIgnoring them may break the game."); noButton = tr("Only Disable Selected"); yesButton = tr("Disable Required"); From f3495bc4c011be93708d2d871bedaccaa930fe18 Mon Sep 17 00:00:00 2001 From: Alexandru Ionut Tripon Date: Thu, 20 Nov 2025 09:27:45 +0200 Subject: [PATCH 14/18] Apply suggestions from code review Co-authored-by: TheKodeToad Signed-off-by: Alexandru Ionut Tripon --- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d711fe0e5..db22bec89 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -429,7 +429,7 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc QString yesButton; if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { title = tr("Confirm toggle"); - message = tr("Toggling this mod(s) will cause changes to other mods.\n") + + message = tr("Toggling these mod(s) will cause changes to other mods.\n") + tr("%n mod(s) will be enabled\n", "", requiredToEnable.size()) + tr("%n mod(s) will be disabled\n", "", requiredToDisable.size()) + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8ea2b7be8..369b9a87d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -142,7 +142,7 @@ void ModFolderPage::removeItems(const QItemSelection& selection) auto affected = m_model->getAffectedMods(indexes, EnableAction::DISABLE); if (!affected.isEmpty()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Disable"), - tr("The mods you are tring to disable are required by %1 mods.\n" + tr("The mods you are trying to delete are required by %1 mods.\n" "Do you want to disable them?") .arg(affected.length()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) From 85071e89190edb52f6749537d022d6af82ccfce3 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 21 Nov 2025 16:18:40 +0200 Subject: [PATCH 15/18] only emit changes on the modified collumns Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index db22bec89..85473c92e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -261,7 +261,7 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) if (result && resource) static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); - emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); } Mod* findById(QSet mods, QString modId) @@ -509,7 +509,7 @@ bool ModFolderModel::deleteResources(const QModelIndexList& indexes) mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiresCount(m_requires[id].count()); int row = m_resources_index[mod->internal_id()]; - emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); } return rsp; } From 3b3e246501083422aa68b13074fc3ed771dd45cf Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 21 Nov 2025 22:23:30 +0200 Subject: [PATCH 16/18] only emit changes when the count changes Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 85473c92e..8b849dc43 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -310,10 +310,12 @@ void ModFolderModel::onParseFinished() } for (auto mod : mods) { auto id = mod->mod_id(); - mod->setRequiredByCount(m_requiredBy[id].count()); - mod->setRequiresCount(m_requires[id].count()); - int row = m_resources_index[mod->internal_id()]; - emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[mod->internal_id()]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } } } @@ -506,10 +508,12 @@ bool ModFolderModel::deleteResources(const QModelIndexList& indexes) auto id = mod->mod_id(); deleteInvalid(m_requiredBy[id]); deleteInvalid(m_requires[id]); - mod->setRequiredByCount(m_requiredBy[id].count()); - mod->setRequiresCount(m_requires[id].count()); - int row = m_resources_index[mod->internal_id()]; - emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[mod->internal_id()]; + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); + } } return rsp; } From fcc557a79d05c5ab2895976755a81f842e211678 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 21 Nov 2025 22:54:04 +0200 Subject: [PATCH 17/18] add cancel to confirm disable dialog Signed-off-by: Trial97 --- launcher/ui/pages/instance/ModFolderPage.cpp | 5 ++++- launcher/ui/widgets/InfoFrame.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 369b9a87d..296ec00ec 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -145,11 +145,14 @@ void ModFolderPage::removeItems(const QItemSelection& selection) tr("The mods you are trying to delete are required by %1 mods.\n" "Do you want to disable them?") .arg(affected.length()), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, + QMessageBox::Cancel) ->exec(); if (response != QMessageBox::Yes) { m_model->setResourceEnabled(affected, EnableAction::DISABLE); + } else if (response != QMessageBox::Cancel) { + return; } } m_model->deleteResources(indexes); diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 2363b6592..1e641c4f9 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -227,7 +227,8 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) setImage(resource_pack.image({ 64, 64 })); } -void InfoFrame::updateWithDataPack(DataPack& data_pack) { +void InfoFrame::updateWithDataPack(DataPack& data_pack) +{ setName(renderColorCodes(data_pack.name())); setDescription(renderColorCodes(data_pack.description())); setImage(data_pack.image({ 64, 64 })); From 38ac238cfaa96af15ccd05fd18e619e2653fb0c1 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 24 Nov 2025 18:37:10 +0200 Subject: [PATCH 18/18] fix packwiz log spam Signed-off-by: Trial97 --- launcher/modplatform/packwiz/Packwiz.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index a14466629..9d049fd32 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -350,7 +350,9 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod if (dep) { ModPlatform::Dependency d; d.addonId = stringEntry(*dep, "addonId"); - d.version = stringEntry(*dep, "version"); + if (dep->contains("version")) { + d.version = stringEntry(*dep, "version"); + } d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type")); mod.dependencies << d; }