From 00e2128fab729e3f6ff2726ea779da65f77b5818 Mon Sep 17 00:00:00 2001 From: crueter Date: Wed, 25 Feb 2026 03:38:13 +0100 Subject: [PATCH] [desktop] Allow deletion of add-ons from the add-on menu (#3626) Adds a location param to the Patch struct which can be used to delete any installed mods at the user's request. You can delete multiple at once too, or just one by right-clicking You are not able to delete game updates, DLC, or SDMC mods. Signed-off-by: crueter Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3626 Reviewed-by: DraVee Reviewed-by: Maufeat --- src/core/file_sys/patch_manager.cpp | 6 +- src/core/file_sys/patch_manager.h | 1 + src/qt_common/abstract/frontend.cpp | 35 ++++---- src/qt_common/abstract/frontend.h | 6 ++ src/qt_common/util/mod.cpp | 1 + src/qt_common/util/mod.h | 2 - .../configure_per_game_addons.cpp | 79 ++++++++++++++++++- .../configuration/configure_per_game_addons.h | 10 +++ 8 files changed, 113 insertions(+), 27 deletions(-) diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 44aafb6b34..82944ceceb 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -876,7 +876,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .type = PatchType::Mod, .program_id = title_id, .title_id = title_id, - .source = PatchSource::Unknown + .source = PatchSource::Unknown, + .location = f->GetFullPath(), }); } @@ -923,7 +924,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .type = PatchType::Mod, .program_id = title_id, .title_id = title_id, - .source = PatchSource::Unknown}); + .source = PatchSource::Unknown, + .location = mod->GetFullPath()}); } } diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index ecd2086984..2be963078d 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -46,6 +46,7 @@ struct Patch { u64 program_id; u64 title_id; PatchSource source; + std::string location; u32 numeric_version{0}; }; diff --git a/src/qt_common/abstract/frontend.cpp b/src/qt_common/abstract/frontend.cpp index 58f1fe6aa8..620256c2d8 100644 --- a/src/qt_common/abstract/frontend.cpp +++ b/src/qt_common/abstract/frontend.cpp @@ -14,41 +14,38 @@ namespace QtCommon::Frontend { -StandardButton ShowMessage( - Icon icon, const QString &title, const QString &text, StandardButtons buttons, QObject *parent) -{ +StandardButton ShowMessage(Icon icon, const QString& title, const QString& text, + StandardButtons buttons, QObject* parent) { #ifdef YUZU_QT_WIDGETS - QMessageBox *box = new QMessageBox(icon, title, text, buttons, (QWidget *) parent); + QMessageBox* box = new QMessageBox(icon, title, text, buttons, (QWidget*)parent); return static_cast(box->exec()); #endif // TODO(crueter): If Qt Widgets is disabled... // need a way to reference icon/buttons too } -const QString GetOpenFileName(const QString &title, - const QString &dir, - const QString &filter, - QString *selectedFilter, - Options options) -{ +const QString GetOpenFileName(const QString& title, const QString& dir, const QString& filter, + QString* selectedFilter, Options options) { #ifdef YUZU_QT_WIDGETS return QFileDialog::getOpenFileName(rootObject, title, dir, filter, selectedFilter, options); #endif } -const QString GetSaveFileName(const QString &title, - const QString &dir, - const QString &filter, - QString *selectedFilter, - Options options) -{ +const QStringList GetOpenFileNames(const QString& title, const QString& dir, const QString& filter, + QString* selectedFilter, Options options) { +#ifdef YUZU_QT_WIDGETS + return QFileDialog::getOpenFileNames(rootObject, title, dir, filter, selectedFilter, options); +#endif +} + +const QString GetSaveFileName(const QString& title, const QString& dir, const QString& filter, + QString* selectedFilter, Options options) { #ifdef YUZU_QT_WIDGETS return QFileDialog::getSaveFileName(rootObject, title, dir, filter, selectedFilter, options); #endif } -const QString GetExistingDirectory(const QString& caption, const QString& dir, - Options options) { +const QString GetExistingDirectory(const QString& caption, const QString& dir, Options options) { #ifdef YUZU_QT_WIDGETS return QFileDialog::getExistingDirectory(rootObject, caption, dir, options); #endif @@ -59,7 +56,7 @@ int Choice(const QString& title, const QString& caption, const QStringList& opti box.setText(caption); box.setWindowTitle(title); - for (const QString &opt : options) { + for (const QString& opt : options) { box.addButton(opt, QMessageBox::AcceptRole); } diff --git a/src/qt_common/abstract/frontend.h b/src/qt_common/abstract/frontend.h index d61eebfeb1..40ef80cbb7 100644 --- a/src/qt_common/abstract/frontend.h +++ b/src/qt_common/abstract/frontend.h @@ -129,6 +129,12 @@ const QString GetOpenFileName(const QString &title, QString *selectedFilter = nullptr, Options options = Options()); +const QStringList GetOpenFileNames(const QString &title, + const QString &dir, + const QString &filter, + QString *selectedFilter = nullptr, + Options options = Options()); + const QString GetSaveFileName(const QString &title, const QString &dir, const QString &filter, diff --git a/src/qt_common/util/mod.cpp b/src/qt_common/util/mod.cpp index f32076fada..1ab05af9b6 100644 --- a/src/qt_common/util/mod.cpp +++ b/src/qt_common/util/mod.cpp @@ -100,6 +100,7 @@ QStringList GetModFolders(const QString& root, const QString& fallbackName) { } else { // Rename the existing mod folder. const auto new_path = std_path.parent_path() / name.toStdString(); + fs::remove_all(new_path); fs::rename(std_path, new_path); std_path = new_path; } diff --git a/src/qt_common/util/mod.h b/src/qt_common/util/mod.h index ceda81ef92..8bdb4bc9dd 100644 --- a/src/qt_common/util/mod.h +++ b/src/qt_common/util/mod.h @@ -4,8 +4,6 @@ #pragma once #include -#include "common/common_types.h" -#include "frontend_common/mod_manager.h" namespace QtCommon::Mod { diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index da84d23876..bdff73a040 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include "common/common_types.h" #include "common/fs/fs.h" @@ -42,14 +42,14 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p item_model = new QStandardItemModel(tree_view); tree_view->setModel(item_model); tree_view->setAlternatingRowColors(true); - tree_view->setSelectionMode(QHeaderView::SingleSelection); + tree_view->setSelectionMode(QHeaderView::MultiSelection); tree_view->setSelectionBehavior(QHeaderView::SelectRows); tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel); tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); tree_view->setSortingEnabled(true); tree_view->setEditTriggers(QHeaderView::NoEditTriggers); tree_view->setUniformRowHeights(true); - tree_view->setContextMenuPolicy(Qt::NoContextMenu); + tree_view->setContextMenuPolicy(Qt::CustomContextMenu); item_model->insertColumns(0, 2); item_model->setHeaderData(0, Qt::Horizontal, tr("Patch Name")); @@ -78,6 +78,8 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p connect(ui->folder, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModFolder); connect(ui->zip, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModZip); + + connect(tree_view, &QTreeView::customContextMenuRequested, this, &ConfigurePerGameAddons::showContextMenu); } ConfigurePerGameAddons::~ConfigurePerGameAddons() = default; @@ -184,6 +186,7 @@ void ConfigurePerGameAddons::InstallModFolder() { } void ConfigurePerGameAddons::InstallModZip() { + // TODO(crueter): use GetOpenFileName to allow select multiple ZIPs const auto path = QtCommon::Frontend::GetOpenFileName( tr("Zipped Mod Location"), QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), @@ -197,6 +200,69 @@ void ConfigurePerGameAddons::InstallModZip() { InstallModPath(extracted, QFileInfo(path).baseName()); } +void ConfigurePerGameAddons::AddonDeleteRequested(QList selected) { + QList filtered; + for (const QModelIndex &index : selected) { + if (!index.data(PATCH_LOCATION).toString().isEmpty()) filtered << index; + } + + if (filtered.empty()) { + QtCommon::Frontend::Critical(tr("Invalid Selection"), + tr("Only mods, cheats, and patches can be deleted.\nTo delete " + "NAND-installed updates, right-click the game in the game " + "list and click Remove -> Remove Installed Update.")); + return; + } + + + const auto header = tr("You are about to delete the following installed mods:\n"); + QString selected_str; + for (const QModelIndex &index : filtered) { + selected_str = selected_str % index.data().toString() % QStringLiteral("\n"); + } + + const auto footer = tr("\nOnce deleted, these can NOT be recovered. Are you 100% sure " + "you want to delete them?"); + + QString caption = header % selected_str % footer; + + auto choice = QtCommon::Frontend::Warning(tr("Delete add-on(s)?"), caption, + QtCommon::Frontend::StandardButton::Yes | + QtCommon::Frontend::StandardButton::No); + + if (choice == QtCommon::Frontend::StandardButton::No) return; + + for (const QModelIndex &index : filtered) { + std::filesystem::remove_all(index.data(PATCH_LOCATION).toString().toStdString()); + } + + QtCommon::Frontend::Information(tr("Successfully deleted"), + tr("Successfully deleted all selected mods.")); + + item_model->removeRows(0, item_model->rowCount()); + list_items.clear(); + LoadConfiguration(); + + UISettings::values.is_game_list_reload_pending.exchange(true); +} + +void ConfigurePerGameAddons::showContextMenu(const QPoint& pos) { + const QModelIndex index = tree_view->indexAt(pos); + auto selected = tree_view->selectionModel()->selectedIndexes(); + if (index.isValid() && selected.empty()) selected = {index}; + + if (selected.empty()) return; + + QMenu menu(this); + + QAction *remove = menu.addAction(tr("&Delete")); + connect(remove, &QAction::triggered, this, [this, selected]() { + AddonDeleteRequested(selected); + }); + + menu.exec(tree_view->viewport()->mapToGlobal(pos)); +} + void ConfigurePerGameAddons::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { RetranslateUI(); @@ -242,8 +308,13 @@ void ConfigurePerGameAddons::LoadConfiguration() { patch.source == FileSys::PatchSource::External && patch.numeric_version != 0; + const bool is_mod = patch.type == FileSys::PatchType::Mod; + if (is_external_update) { - first_item->setData(static_cast(patch.numeric_version), Qt::UserRole); + first_item->setData(static_cast(patch.numeric_version), NUMERIC_VERSION); + } else if (is_mod) { + // qDebug() << patch.location; + first_item->setData(QString::fromStdString(patch.location), PATCH_LOCATION); } bool patch_disabled = false; diff --git a/src/yuzu/configuration/configure_per_game_addons.h b/src/yuzu/configuration/configure_per_game_addons.h index d2f361139b..20ab39541b 100644 --- a/src/yuzu/configuration/configure_per_game_addons.h +++ b/src/yuzu/configuration/configure_per_game_addons.h @@ -32,6 +32,11 @@ class ConfigurePerGameAddons : public QWidget { Q_OBJECT public: + enum PatchData { + NUMERIC_VERSION = Qt::UserRole, + PATCH_LOCATION + }; + explicit ConfigurePerGameAddons(Core::System& system_, QWidget* parent = nullptr); ~ConfigurePerGameAddons() override; @@ -49,6 +54,11 @@ public slots: void InstallModFolder(); void InstallModZip(); + void AddonDeleteRequested(QList selected); + +protected: + void showContextMenu(const QPoint& pos); + private: void changeEvent(QEvent* event) override; void RetranslateUI();