Browse Source

[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 <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3626
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
xbzk/flicker-fix
crueter 4 days ago
parent
commit
00e2128fab
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 6
      src/core/file_sys/patch_manager.cpp
  2. 1
      src/core/file_sys/patch_manager.h
  3. 35
      src/qt_common/abstract/frontend.cpp
  4. 6
      src/qt_common/abstract/frontend.h
  5. 1
      src/qt_common/util/mod.cpp
  6. 2
      src/qt_common/util/mod.h
  7. 79
      src/yuzu/configuration/configure_per_game_addons.cpp
  8. 10
      src/yuzu/configuration/configure_per_game_addons.h

6
src/core/file_sys/patch_manager.cpp

@ -876,7 +876,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.type = PatchType::Mod, .type = PatchType::Mod,
.program_id = title_id, .program_id = title_id,
.title_id = title_id, .title_id = title_id,
.source = PatchSource::Unknown
.source = PatchSource::Unknown,
.location = f->GetFullPath(),
}); });
} }
@ -923,7 +924,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.type = PatchType::Mod, .type = PatchType::Mod,
.program_id = title_id, .program_id = title_id,
.title_id = title_id, .title_id = title_id,
.source = PatchSource::Unknown});
.source = PatchSource::Unknown,
.location = mod->GetFullPath()});
} }
} }

1
src/core/file_sys/patch_manager.h

@ -46,6 +46,7 @@ struct Patch {
u64 program_id; u64 program_id;
u64 title_id; u64 title_id;
PatchSource source; PatchSource source;
std::string location;
u32 numeric_version{0}; u32 numeric_version{0};
}; };

35
src/qt_common/abstract/frontend.cpp

@ -14,41 +14,38 @@
namespace QtCommon::Frontend { 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 #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<QMessageBox::StandardButton>(box->exec()); return static_cast<QMessageBox::StandardButton>(box->exec());
#endif #endif
// TODO(crueter): If Qt Widgets is disabled... // TODO(crueter): If Qt Widgets is disabled...
// need a way to reference icon/buttons too // 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 #ifdef YUZU_QT_WIDGETS
return QFileDialog::getOpenFileName(rootObject, title, dir, filter, selectedFilter, options); return QFileDialog::getOpenFileName(rootObject, title, dir, filter, selectedFilter, options);
#endif #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 #ifdef YUZU_QT_WIDGETS
return QFileDialog::getSaveFileName(rootObject, title, dir, filter, selectedFilter, options); return QFileDialog::getSaveFileName(rootObject, title, dir, filter, selectedFilter, options);
#endif #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 #ifdef YUZU_QT_WIDGETS
return QFileDialog::getExistingDirectory(rootObject, caption, dir, options); return QFileDialog::getExistingDirectory(rootObject, caption, dir, options);
#endif #endif
@ -59,7 +56,7 @@ int Choice(const QString& title, const QString& caption, const QStringList& opti
box.setText(caption); box.setText(caption);
box.setWindowTitle(title); box.setWindowTitle(title);
for (const QString &opt : options) {
for (const QString& opt : options) {
box.addButton(opt, QMessageBox::AcceptRole); box.addButton(opt, QMessageBox::AcceptRole);
} }

6
src/qt_common/abstract/frontend.h

@ -129,6 +129,12 @@ const QString GetOpenFileName(const QString &title,
QString *selectedFilter = nullptr, QString *selectedFilter = nullptr,
Options options = Options()); 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 GetSaveFileName(const QString &title,
const QString &dir, const QString &dir,
const QString &filter, const QString &filter,

1
src/qt_common/util/mod.cpp

@ -100,6 +100,7 @@ QStringList GetModFolders(const QString& root, const QString& fallbackName) {
} else { } else {
// Rename the existing mod folder. // Rename the existing mod folder.
const auto new_path = std_path.parent_path() / name.toStdString(); const auto new_path = std_path.parent_path() / name.toStdString();
fs::remove_all(new_path);
fs::rename(std_path, new_path); fs::rename(std_path, new_path);
std_path = new_path; std_path = new_path;
} }

2
src/qt_common/util/mod.h

@ -4,8 +4,6 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include "common/common_types.h"
#include "frontend_common/mod_manager.h"
namespace QtCommon::Mod { namespace QtCommon::Mod {

79
src/yuzu/configuration/configure_per_game_addons.cpp

@ -16,7 +16,7 @@
#include <QString> #include <QString>
#include <QTimer> #include <QTimer>
#include <QTreeView> #include <QTreeView>
#include <qstandardpaths.h>
#include <QStandardPaths>
#include "common/common_types.h" #include "common/common_types.h"
#include "common/fs/fs.h" #include "common/fs/fs.h"
@ -42,14 +42,14 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p
item_model = new QStandardItemModel(tree_view); item_model = new QStandardItemModel(tree_view);
tree_view->setModel(item_model); tree_view->setModel(item_model);
tree_view->setAlternatingRowColors(true); tree_view->setAlternatingRowColors(true);
tree_view->setSelectionMode(QHeaderView::SingleSelection);
tree_view->setSelectionMode(QHeaderView::MultiSelection);
tree_view->setSelectionBehavior(QHeaderView::SelectRows); tree_view->setSelectionBehavior(QHeaderView::SelectRows);
tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel); tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
tree_view->setSortingEnabled(true); tree_view->setSortingEnabled(true);
tree_view->setEditTriggers(QHeaderView::NoEditTriggers); tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
tree_view->setUniformRowHeights(true); tree_view->setUniformRowHeights(true);
tree_view->setContextMenuPolicy(Qt::NoContextMenu);
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
item_model->insertColumns(0, 2); item_model->insertColumns(0, 2);
item_model->setHeaderData(0, Qt::Horizontal, tr("Patch Name")); 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->folder, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModFolder);
connect(ui->zip, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModZip); connect(ui->zip, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModZip);
connect(tree_view, &QTreeView::customContextMenuRequested, this, &ConfigurePerGameAddons::showContextMenu);
} }
ConfigurePerGameAddons::~ConfigurePerGameAddons() = default; ConfigurePerGameAddons::~ConfigurePerGameAddons() = default;
@ -184,6 +186,7 @@ void ConfigurePerGameAddons::InstallModFolder() {
} }
void ConfigurePerGameAddons::InstallModZip() { void ConfigurePerGameAddons::InstallModZip() {
// TODO(crueter): use GetOpenFileName to allow select multiple ZIPs
const auto path = QtCommon::Frontend::GetOpenFileName( const auto path = QtCommon::Frontend::GetOpenFileName(
tr("Zipped Mod Location"), tr("Zipped Mod Location"),
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), QStandardPaths::writableLocation(QStandardPaths::DownloadLocation),
@ -197,6 +200,69 @@ void ConfigurePerGameAddons::InstallModZip() {
InstallModPath(extracted, QFileInfo(path).baseName()); InstallModPath(extracted, QFileInfo(path).baseName());
} }
void ConfigurePerGameAddons::AddonDeleteRequested(QList<QModelIndex> selected) {
QList<QModelIndex> 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) { void ConfigurePerGameAddons::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) { if (event->type() == QEvent::LanguageChange) {
RetranslateUI(); RetranslateUI();
@ -242,8 +308,13 @@ void ConfigurePerGameAddons::LoadConfiguration() {
patch.source == FileSys::PatchSource::External && patch.source == FileSys::PatchSource::External &&
patch.numeric_version != 0; patch.numeric_version != 0;
const bool is_mod = patch.type == FileSys::PatchType::Mod;
if (is_external_update) { if (is_external_update) {
first_item->setData(static_cast<quint32>(patch.numeric_version), Qt::UserRole);
first_item->setData(static_cast<quint32>(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; bool patch_disabled = false;

10
src/yuzu/configuration/configure_per_game_addons.h

@ -32,6 +32,11 @@ class ConfigurePerGameAddons : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
enum PatchData {
NUMERIC_VERSION = Qt::UserRole,
PATCH_LOCATION
};
explicit ConfigurePerGameAddons(Core::System& system_, QWidget* parent = nullptr); explicit ConfigurePerGameAddons(Core::System& system_, QWidget* parent = nullptr);
~ConfigurePerGameAddons() override; ~ConfigurePerGameAddons() override;
@ -49,6 +54,11 @@ public slots:
void InstallModFolder(); void InstallModFolder();
void InstallModZip(); void InstallModZip();
void AddonDeleteRequested(QList<QModelIndex> selected);
protected:
void showContextMenu(const QPoint& pos);
private: private:
void changeEvent(QEvent* event) override; void changeEvent(QEvent* event) override;
void RetranslateUI(); void RetranslateUI();

Loading…
Cancel
Save