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();