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