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/minecraft/mod/Mod.cpp | 74 +++++++----------------------------------- 1 file changed, 11 insertions(+), 63 deletions(-) (limited to 'launcher/minecraft/mod/Mod.cpp') 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); -- 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(-) (limited to 'launcher/minecraft/mod/Mod.cpp') 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 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(-) (limited to 'launcher/minecraft/mod/Mod.cpp') 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 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(-) (limited to 'launcher/minecraft/mod/Mod.cpp') 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