From 1e2f0ab3083f002071938275a97b13c0c4633e64 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 10 Aug 2022 14:42:24 -0300 Subject: [PATCH] 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 ++++ .../minecraft/mod/ResourceFolderModel.cpp | 114 +++++++++++++++++- launcher/minecraft/mod/ResourceFolderModel.h | 42 ++++++- 8 files changed, 265 insertions(+), 16 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index f28fd32a6..ed91d999f 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 313c478c5..25ac88c98 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 8ab604139..9b8c58bc9 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 ea9f0000d..b1f307108 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 8771a20f6..c58df3d87 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 c348c7e2c..68663ab0f 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 4867a8c2d..982915e2f 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 31fd74149..2ccf14f0e 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;