qt: Add option to uninstall a game. (#7064)

* qt: Add option to uninstall a game.

* Address review comments.
This commit is contained in:
Steveice10 2023-10-14 18:11:59 -07:00 committed by GitHub
parent 3d55270de6
commit 07839fb3ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 223 additions and 46 deletions

View file

@ -16,6 +16,7 @@
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QModelIndex>
#include <QStandardItem>
#include <QStandardItemModel>
@ -459,8 +460,11 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::Game:
AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(),
selected.data(GameListItemPath::TitleRole).toString(),
selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
selected.data(GameListItemPath::ExtdataIdRole).toULongLong());
selected.data(GameListItemPath::ExtdataIdRole).toULongLong(),
static_cast<Service::FS::MediaType>(
selected.data(GameListItemPath::MediaTypeRole).toUInt()));
break;
case GameListItemType::CustomDir:
AddPermDirPopup(context_menu, selected);
@ -522,28 +526,36 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) {
}
}
void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id,
u64 extdata_id) {
void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name,
u64 program_id, u64 extdata_id, Service::FS::MediaType media_type) {
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location"));
QAction* open_application_location = context_menu.addAction(tr("Open Application Location"));
QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location"));
QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location"));
QAction* open_texture_dump_location = context_menu.addAction(tr("Open Texture Dump Location"));
QAction* open_texture_load_location =
context_menu.addAction(tr("Open Custom Texture Location"));
QAction* open_mods_location = context_menu.addAction(tr("Open Mods Location"));
QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location"));
QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache"));
QAction* dump_romfs = context_menu.addAction(tr("Dump RomFS"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Properties"));
QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache"));
QAction* open_shader_cache_location = shader_menu->addAction(tr("Open Shader Cache Location"));
shader_menu->addSeparator();
QAction* delete_opengl_disk_shader_cache =
shader_menu->addAction(tr("Delete OpenGL Shader Cache"));
QMenu* uninstall_menu = context_menu.addMenu(tr("Uninstall"));
QAction* uninstall_all = uninstall_menu->addAction(tr("Everything"));
uninstall_menu->addSeparator();
QAction* uninstall_game = uninstall_menu->addAction(tr("Game"));
QAction* uninstall_update = uninstall_menu->addAction(tr("Update"));
QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Properties"));
const u32 program_id_high = (program_id >> 32) & 0xFFFFFFFF;
const bool is_application = program_id_high == 0x00040000 || program_id_high == 0x00040010;
@ -564,22 +576,36 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra
open_extdata_location->setVisible(false);
}
auto media_type = Service::AM::GetTitleMediaType(program_id);
open_application_location->setEnabled(path.toStdString() ==
Service::AM::GetTitleContentPath(media_type, program_id));
open_update_location->setEnabled(
is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC,
program_id + 0xe00000000) +
"content/"));
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
const auto update_program_id = program_id | 0xE00000000;
const auto dlc_program_id = program_id | 0x8C00000000;
const auto is_installed =
media_type == Service::FS::MediaType::NAND || media_type == Service::FS::MediaType::SDMC;
const auto has_update =
is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC,
update_program_id) +
"content/");
const auto has_dlc =
is_application &&
FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, dlc_program_id) +
"content/");
open_application_location->setEnabled(is_installed);
open_update_location->setEnabled(has_update);
open_dlc_location->setEnabled(has_dlc);
open_texture_dump_location->setEnabled(is_application);
open_texture_load_location->setEnabled(is_application);
open_mods_location->setEnabled(is_application);
open_dlc_location->setEnabled(is_application);
dump_romfs->setEnabled(is_application);
delete_opengl_disk_shader_cache->setEnabled(opengl_cache_exists);
uninstall_all->setEnabled(is_installed || has_update || has_dlc);
uninstall_game->setEnabled(is_installed);
uninstall_update->setEnabled(has_update);
uninstall_dlc->setEnabled(has_dlc);
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
connect(open_save_location, &QAction::triggered, this, [this, program_id] {
@ -641,7 +667,63 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra
connect(delete_opengl_disk_shader_cache, &QAction::triggered, this, [program_id] {
ForEachOpenGLCacheFile(program_id, [](QFile& file) { file.remove(); });
});
};
connect(uninstall_all, &QAction::triggered, this, [=, this] {
QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Citra"),
tr("Are you sure you want to completely uninstall '%1'?\n\nThis will "
"delete the game if installed, as well as any installed updates or DLC.")
.arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer == QMessageBox::Yes) {
std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles;
if (is_installed) {
titles.emplace_back(media_type, program_id, name);
}
if (has_update) {
titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id,
tr("%1 (Update)").arg(name));
}
if (has_dlc) {
titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id,
tr("%1 (DLC)").arg(name));
}
main_window->UninstallTitles(titles);
}
});
connect(uninstall_game, &QAction::triggered, this, [this, name, media_type, program_id] {
QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Citra"), tr("Are you sure you want to uninstall '%1'?").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer == QMessageBox::Yes) {
std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles;
titles.emplace_back(media_type, program_id, name);
main_window->UninstallTitles(titles);
}
});
connect(uninstall_update, &QAction::triggered, this, [this, name, update_program_id] {
QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Citra"),
tr("Are you sure you want to uninstall the update for '%1'?").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer == QMessageBox::Yes) {
std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles;
titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id,
tr("%1 (Update)").arg(name));
main_window->UninstallTitles(titles);
}
});
connect(uninstall_dlc, &QAction::triggered, this, [this, name, dlc_program_id] {
QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Citra"), tr("Are you sure you want to uninstall all DLC for '%1'?").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer == QMessageBox::Yes) {
std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles;
titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id,
tr("%1 (DLC)").arg(name));
main_window->UninstallTitles(titles);
}
});
}
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
UISettings::GameDir& game_dir =

View file

@ -12,6 +12,10 @@
#include "common/common_types.h"
#include "uisettings.h"
namespace Service::FS {
enum class MediaType : u32;
}
class GameListWorker;
class GameListDir;
class GameListSearchField;
@ -105,7 +109,8 @@ private:
void PopupContextMenu(const QPoint& menu_location);
void PopupHeaderContextMenu(const QPoint& menu_location);
void AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, u64 extdata_id);
void AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, u64 program_id,
u64 extdata_id, Service::FS::MediaType media_type);
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
void UpdateColumnVisibility();

View file

@ -25,6 +25,10 @@
#include "common/string_util.h"
#include "core/loader/smdh.h"
namespace Service::FS {
enum class MediaType : u32;
}
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
CustomDir = QStandardItem::UserType + 2,
@ -153,14 +157,16 @@ public:
static constexpr int ProgramIdRole = SortRole + 3;
static constexpr int ExtdataIdRole = SortRole + 4;
static constexpr int LongTitleRole = SortRole + 5;
static constexpr int MediaTypeRole = SortRole + 6;
GameListItemPath() = default;
GameListItemPath(const QString& game_path, std::span<const u8> smdh_data, u64 program_id,
u64 extdata_id) {
u64 extdata_id, Service::FS::MediaType media_type) {
setData(type(), TypeRole);
setData(game_path, FullPathRole);
setData(qulonglong(program_id), ProgramIdRole);
setData(qulonglong(extdata_id), ExtdataIdRole);
setData(quint32(media_type), MediaTypeRole);
if (UISettings::values.game_list_icon_size.GetValue() ==
UISettings::GameListIconSize::NoIcon) {

View file

@ -33,10 +33,11 @@ GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
GameListWorker::~GameListWorker() = default;
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
GameListDir* parent_dir) {
const auto callback = [this, recursion, parent_dir](u64* num_entries_out,
const std::string& directory,
const std::string& virtual_name) -> bool {
GameListDir* parent_dir,
Service::FS::MediaType media_type) {
const auto callback = [this, recursion, parent_dir,
media_type](u64* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool {
if (stop_processing) {
// Breaks the callback loop.
return false;
@ -105,7 +106,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
emit EntryReady(
{
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id,
extdata_id),
extdata_id, media_type),
new GameListItemCompat(compatibility),
new GameListItemRegion(smdh),
new GameListItem(
@ -116,7 +117,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
} else if (is_dir && recursion > 0) {
watch_list.append(QString::fromStdString(physical_name));
AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir);
AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir, media_type);
}
return true;
@ -144,8 +145,10 @@ void GameListWorker::run() {
watch_list.append(demos_path);
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
emit DirEntryReady(game_list_dir);
AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir);
AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir);
AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir,
Service::FS::MediaType::SDMC);
AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir,
Service::FS::MediaType::SDMC);
} else if (game_dir.path == QStringLiteral("SYSTEM")) {
QString path =
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) +
@ -153,13 +156,14 @@ void GameListWorker::run() {
watch_list.append(path);
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
emit DirEntryReady(game_list_dir);
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir);
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir,
Service::FS::MediaType::NAND);
} else {
watch_list.append(game_dir.path);
auto* const game_list_dir = new GameListDir(game_dir);
emit DirEntryReady(game_list_dir);
AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0,
game_list_dir);
game_list_dir, Service::FS::MediaType::GameCard);
}
}

View file

@ -15,6 +15,10 @@
#include "citra_qt/compatibility_list.h"
#include "common/common_types.h"
namespace Service::FS {
enum class MediaType : u32;
}
class QStandardItem;
/**
@ -52,7 +56,7 @@ signals:
private:
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
GameListDir* parent_dir);
GameListDir* parent_dir, Service::FS::MediaType media_type);
QVector<UISettings::GameDir>& game_dirs;
const CompatibilityList& compatibility_list;

View file

@ -10,6 +10,7 @@
#include <QLabel>
#include <QMessageBox>
#include <QSysInfo>
#include <QtConcurrent/QtConcurrentMap>
#include <QtConcurrent/QtConcurrentRun>
#include <QtGui>
#include <QtWidgets>
@ -1750,6 +1751,57 @@ void GMainWindow::OnCIAInstallFinished() {
game_list->PopulateAsync(UISettings::values.game_dirs);
}
void GMainWindow::UninstallTitles(
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles) {
if (titles.empty()) {
return;
}
// Select the first title in the list as representative.
const auto first_name = std::get<QString>(titles[0]);
QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0,
static_cast<int>(titles.size()), this);
progress.setWindowModality(Qt::WindowModal);
QFutureWatcher<void> future_watcher;
QObject::connect(&future_watcher, &QFutureWatcher<void>::finished, &progress,
&QProgressDialog::reset);
QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher,
&QFutureWatcher<void>::cancel);
QObject::connect(&future_watcher, &QFutureWatcher<void>::progressValueChanged, &progress,
&QProgressDialog::setValue);
auto failed = false;
QString failed_name;
const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) {
const auto name = std::get<QString>(title);
const auto media_type = std::get<Service::FS::MediaType>(title);
const auto program_id = std::get<u64>(title);
const auto result = Service::AM::UninstallProgram(media_type, program_id);
if (result.IsError()) {
LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(),
result.raw);
failed = true;
failed_name = name;
future_watcher.cancel();
}
};
future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title));
progress.exec();
future_watcher.waitForFinished();
if (failed) {
QMessageBox::critical(this, tr("Citra"), tr("Failed to uninstall '%1'.").arg(failed_name));
} else if (!future_watcher.isCanceled()) {
QMessageBox::information(this, tr("Citra"),
tr("Successfully uninstalled '%1'.").arg(first_name));
}
}
void GMainWindow::OnMenuRecentFile() {
QAction* action = qobject_cast<QAction*>(sender());
ASSERT(action);

View file

@ -73,6 +73,10 @@ namespace Service::AM {
enum class InstallStatus : u32;
}
namespace Service::FS {
enum class MediaType : u32;
}
class GMainWindow : public QMainWindow {
Q_OBJECT
@ -100,6 +104,9 @@ public:
bool DropAction(QDropEvent* event);
void AcceptDropEvent(QDropEvent* event);
void UninstallTitles(
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles);
public slots:
void OnAppFocusStateChanged(Qt::ApplicationState state);
void OnLoadComplete();

View file

@ -1557,24 +1557,33 @@ void Module::Interface::GetRequiredSizeFromCia(Kernel::HLERequestContext& ctx) {
rb.Push(container.GetTitleMetadata().GetContentSizeByIndex(FileSys::TMDContentIndex::Main));
}
ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id) {
// Use the content folder so we don't delete the user's save data.
const auto path = GetTitlePath(media_type, title_id) + "content/";
if (!FileUtil::Exists(path)) {
return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent};
}
if (!FileUtil::DeleteDirRecursively(path)) {
// TODO: Determine the right error code for this.
return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent};
}
return RESULT_SUCCESS;
}
void Module::Interface::DeleteProgram(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
auto media_type = rp.PopEnum<FS::MediaType>();
u64 title_id = rp.Pop<u64>();
LOG_INFO(Service_AM, "Deleting title 0x{:016x}", title_id);
std::string path = GetTitlePath(media_type, title_id);
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
if (!FileUtil::Exists(path)) {
rb.Push(ResultCode(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent));
LOG_ERROR(Service_AM, "Title not found");
return;
}
bool success = FileUtil::DeleteDirRecursively(path);
const auto media_type = rp.PopEnum<FS::MediaType>();
const auto title_id = rp.Pop<u64>();
LOG_INFO(Service_AM, "called, title={:016x}", title_id);
const auto result = UninstallProgram(media_type, title_id);
am->ScanForAllTitles();
rb.Push(RESULT_SUCCESS);
if (!success)
LOG_ERROR(Service_AM, "FileUtil::DeleteDirRecursively unexpectedly failed");
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(result);
}
void Module::Interface::GetSystemUpdaterMutex(Kernel::HLERequestContext& ctx) {

View file

@ -172,6 +172,14 @@ std::string GetTitlePath(Service::FS::MediaType media_type, u64 tid);
*/
std::string GetMediaTitlePath(Service::FS::MediaType media_type);
/**
* Uninstalls the specified title.
* @param media_type the storage medium the title is installed to
* @param title_id the title ID to uninstall
* @return result of the uninstall operation
*/
ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id);
class Module final {
public:
explicit Module(Core::System& system);