From 1eb0b2260d6d47e21f0066df34cbb91068e7275d Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 6 Jan 2026 14:19:59 +0100 Subject: [PATCH] Refactor game_list_frame --- rpcs3/rpcs3.vcxproj | 36 +- rpcs3/rpcs3.vcxproj.filters | 24 + rpcs3/rpcs3qt/CMakeLists.txt | 2 + rpcs3/rpcs3qt/game_list_actions.cpp | 1604 ++++++++++++++ rpcs3/rpcs3qt/game_list_actions.h | 113 + rpcs3/rpcs3qt/game_list_context_menu.cpp | 907 ++++++++ rpcs3/rpcs3qt/game_list_context_menu.h | 32 + rpcs3/rpcs3qt/game_list_frame.cpp | 2457 +--------------------- rpcs3/rpcs3qt/game_list_frame.h | 144 +- rpcs3/rpcs3qt/game_list_grid.cpp | 2 - rpcs3/rpcs3qt/main_window.cpp | 24 +- 11 files changed, 2767 insertions(+), 2578 deletions(-) create mode 100644 rpcs3/rpcs3qt/game_list_actions.cpp create mode 100644 rpcs3/rpcs3qt/game_list_actions.h create mode 100644 rpcs3/rpcs3qt/game_list_context_menu.cpp create mode 100644 rpcs3/rpcs3qt/game_list_context_menu.h diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 9bef8696fe..028f2e74c8 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -289,6 +289,12 @@ true + + true + + + true + true @@ -586,6 +592,12 @@ true + + true + + + true + true @@ -833,7 +845,9 @@ + + @@ -1458,6 +1472,26 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) @@ -2236,4 +2270,4 @@ - + \ No newline at end of file diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 5d9f844e63..100a9b1d8f 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1254,6 +1254,24 @@ Io\camera + + Gui\game list + + + Gui\game list + + + Generated Files\Debug + + + Generated Files\Debug + + + Generated Files\Release + + + Generated Files\Release + @@ -1837,6 +1855,12 @@ Gui\debugger + + Gui\game list + + + Gui\game list + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 9352f58823..5b74a77a59 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -31,7 +31,9 @@ add_library(rpcs3_ui STATIC flow_widget_item.cpp game_compatibility.cpp game_list.cpp + game_list_actions.cpp game_list_base.cpp + game_list_context_menu.cpp game_list_delegate.cpp game_list_frame.cpp game_list_grid.cpp diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp new file mode 100644 index 0000000000..3e8a57d703 --- /dev/null +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -0,0 +1,1604 @@ +#include "stdafx.h" +#include "game_list_actions.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "category.h" +#include "qt_utils.h" +#include "progress_dialog.h" + +#include "Utilities/File.h" + +#include "Emu/System.h" +#include "Emu/system_utils.hpp" +#include "Emu/VFS.h" +#include "Emu/vfs_config.h" + +#include "Input/pad_thread.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_CHANNEL(game_list_log, "GameList"); + +extern atomic_t g_system_progress_canceled; + +game_list_actions::game_list_actions(game_list_frame* frame, std::shared_ptr gui_settings) + : m_game_list_frame(frame), m_gui_settings(std::move(gui_settings)) +{ + ensure(!!m_game_list_frame); + ensure(!!m_gui_settings); +} + +game_list_actions::~game_list_actions() +{ +} + +void game_list_actions::SetContentList(u16 content_types, const content_info& content_info) +{ + m_content_info = content_info; + + m_content_info.content_types = content_types; + m_content_info.clear_on_finish = true; // Always overridden by BatchRemoveContentLists() +} + +void game_list_actions::ClearContentList(bool refresh) +{ + if (refresh) + { + std::vector serials_to_remove_from_yml; + + // Prepare the list of serials (title id) to remove in "games.yml" file (if any) + for (const auto& removedDisc : m_content_info.removed_disc_list) + { + serials_to_remove_from_yml.push_back(removedDisc); + } + + // Finally, refresh the game list + m_game_list_frame->Refresh(true, serials_to_remove_from_yml); + } + + m_content_info = {NO_CONTENT}; +} + +game_list_actions::content_info game_list_actions::GetContentInfo(const std::vector& games) +{ + content_info content_info = {NO_CONTENT}; + + if (games.empty()) + return content_info; + + bool is_disc_game = false; + u64 total_disc_size = 0; + u64 total_data_size = 0; + QString text; + + // Fill in content_info + + content_info.is_single_selection = games.size() == 1; + + for (const auto& game : games) + { + GameInfo& current_game = game->info; + + is_disc_game = QString::fromStdString(current_game.category) == cat::cat_disc_game; + + // +1 if it's a disc game's path and it's present in the shared games folder + content_info.in_games_dir_count += (is_disc_game && Emu.IsPathInsideDir(current_game.path, rpcs3::utils::get_games_dir())) ? 1 : 0; + + // Add the name to the content's name list for the related serial + content_info.name_list[current_game.serial].insert(current_game.name); + + if (is_disc_game) + { + if (current_game.size_on_disk != umax) // If size was properly detected + total_disc_size += current_game.size_on_disk; + + // Add the serial to the disc list + content_info.disc_list.insert(current_game.serial); + + // It could be an empty list for a disc game + std::set data_dir_list = rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd0_game_dir(), current_game.serial); + + // Add the path list to the content's path list for the related serial + for (const auto& data_dir : data_dir_list) + { + content_info.path_list[current_game.serial].insert(data_dir); + } + } + else + { + // Add the path to the content's path list for the related serial + content_info.path_list[current_game.serial].insert(current_game.path); + } + } + + // Fill in text based on filled in content_info + + if (content_info.is_single_selection) // Single selection + { + GameInfo& current_game = games[0]->info; + + text = tr("%0 - %1\n").arg(QString::fromStdString(current_game.serial)).arg(QString::fromStdString(current_game.name)); + + if (is_disc_game) + { + text += tr("\nDisc Game Info:\nPath: %0\n").arg(QString::fromStdString(current_game.path)); + + if (total_disc_size) + text += tr("Size: %0\n").arg(gui::utils::format_byte_size(total_disc_size)); + } + + // if a path is present (it could be an empty list for a disc game) + if (const auto& it = content_info.path_list.find(current_game.serial); it != content_info.path_list.end()) + { + text += tr("\n%0 Info:\n").arg(is_disc_game ? tr("Game Data") : games[0]->localized_category); + + for (const auto& data_dir : it->second) + { + text += tr("Path: %0\n").arg(QString::fromStdString(data_dir)); + + if (const u64 data_size = fs::get_dir_size(data_dir, 1); data_size != umax) + { // If size was properly detected + total_data_size += data_size; + text += tr("Size: %0\n").arg(gui::utils::format_byte_size(data_size)); + } + } + + if (it->second.size() > 1) + text += tr("Total size: %0\n").arg(gui::utils::format_byte_size(total_data_size)); + } + } + else // Multi selection + { + for (const auto& [serial, data_dir_list] : content_info.path_list) + { + for (const auto& data_dir : data_dir_list) + { + if (const u64 data_size = fs::get_dir_size(data_dir, 1); data_size != umax) // If size was properly detected + total_data_size += data_size; + } + } + + text = tr("%0 selected games: %1 Disc Game - %2 not Disc Game\n").arg(games.size()) + .arg(content_info.disc_list.size()).arg(games.size() - content_info.disc_list.size()); + + text += tr("\nDisc Game Info:\n"); + + if (content_info.disc_list.size() != content_info.in_games_dir_count) + text += tr("VFS unhosted: %0\n").arg(content_info.disc_list.size() - content_info.in_games_dir_count); + + if (content_info.in_games_dir_count) + text += tr("VFS hosted: %0\n").arg(content_info.in_games_dir_count); + + if (content_info.disc_list.size() != content_info.in_games_dir_count && content_info.in_games_dir_count) + text += tr("Total games: %0\n").arg((content_info.disc_list.size() - content_info.in_games_dir_count) + content_info.in_games_dir_count); + + if (total_disc_size) + text += tr("Total size: %0\n").arg(gui::utils::format_byte_size(total_disc_size)); + + if (content_info.path_list.size()) + text += tr("\nGame Data Info:\nTotal size: %0\n").arg(gui::utils::format_byte_size(total_data_size)); + } + + u64 caches_size = 0; + u64 icons_size = 0; + u64 savestates_size = 0; + u64 captures_size = 0; + u64 recordings_size = 0; + u64 screenshots_size = 0; + + for (const auto& [serial, name_list] : content_info.name_list) + { + // Main cache + if (const u64 size = fs::get_dir_size(rpcs3::utils::get_cache_dir_by_serial(serial), 1); size != umax) + caches_size += size; + + // HDD1 cache + for (const auto& dir : rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd1_cache_dir(), serial)) + { + if (const u64 size = fs::get_dir_size(dir, 1); size != umax) + caches_size += size; + } + + if (const u64 size = fs::get_dir_size(rpcs3::utils::get_icons_dir(serial), 1); size != umax) + icons_size += size; + + if (const u64 size = fs::get_dir_size(rpcs3::utils::get_savestates_dir(serial), 1); size != umax) + savestates_size += size; + + for (const auto& file : rpcs3::utils::get_file_list(rpcs3::utils::get_captures_dir(), serial)) + { + if (fs::stat_t stat{}; fs::get_stat(file, stat)) + captures_size += stat.size; + } + + if (const u64 size = fs::get_dir_size(rpcs3::utils::get_recordings_dir(serial), 1); size != umax) + recordings_size += size; + + if (const u64 size = fs::get_dir_size(rpcs3::utils::get_screenshots_dir(serial), 1); size != umax) + screenshots_size += size; + } + + text += tr("\nEmulator Data Info:\nCaches size: %0\n").arg(gui::utils::format_byte_size(caches_size)); + text += tr("Icons size: %0\n").arg(gui::utils::format_byte_size(icons_size)); + text += tr("Savestates size: %0\n").arg(gui::utils::format_byte_size(savestates_size)); + text += tr("Captures size: %0\n").arg(gui::utils::format_byte_size(captures_size)); + text += tr("Recordings size: %0\n").arg(gui::utils::format_byte_size(recordings_size)); + text += tr("Screenshots size: %0\n").arg(gui::utils::format_byte_size(screenshots_size)); + + // Retrieve disk space info on data path's drive + if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat)) + text += tr("\nCurrent free disk space: %0\n").arg(gui::utils::format_byte_size(stat.avail_free)); + + content_info.info = text; + + return content_info; +} + +void game_list_actions::ShowRemoveGameDialog(const std::vector& games) +{ + if (games.empty()) + return; + + content_info content_info = GetContentInfo(games); + QString text = content_info.info; + + QCheckBox* disc = new QCheckBox(tr("Remove title from game list (Disc Game path is not removed!)")); + QCheckBox* caches = new QCheckBox(tr("Remove caches and custom configs")); + QCheckBox* icons = new QCheckBox(tr("Remove icons and shortcuts")); + QCheckBox* savestate = new QCheckBox(tr("Remove savestates")); + QCheckBox* captures = new QCheckBox(tr("Remove captures")); + QCheckBox* recordings = new QCheckBox(tr("Remove recordings")); + QCheckBox* screenshots = new QCheckBox(tr("Remove screenshots")); + + if (content_info.disc_list.size()) + { + if (content_info.in_games_dir_count == content_info.disc_list.size()) + { + disc->setToolTip(tr("Title located under auto-detection VFS \"games\" folder cannot be removed")); + disc->setDisabled(true); + } + else + { + if (!content_info.is_single_selection) // Multi selection + disc->setToolTip(tr("Title located under auto-detection VFS \"games\" folder cannot be removed")); + + disc->setChecked(true); + } + } + else + { + disc->setChecked(false); + disc->setVisible(false); + } + + if (content_info.path_list.size()) // If a path is present + { + text += tr("\nPermanently remove %0 and selected (optional) contents from drive?\n") + .arg((content_info.disc_list.size() || !content_info.is_single_selection) ? tr("Game Data") : games[0]->localized_category); + } + else + { + text += tr("\nPermanently remove selected (optional) contents from drive?\n"); + } + + caches->setChecked(true); + icons->setChecked(true); + + QMessageBox mb(QMessageBox::Question, tr("Confirm Removal"), text, QMessageBox::Yes | QMessageBox::No, m_game_list_frame); + mb.setCheckBox(disc); + + QGridLayout* grid = qobject_cast(mb.layout()); + int row, column, rowSpan, columnSpan; + + grid->getItemPosition(grid->indexOf(disc), &row, &column, &rowSpan, &columnSpan); + grid->addWidget(caches, row + 3, column, rowSpan, columnSpan); + grid->addWidget(icons, row + 4, column, rowSpan, columnSpan); + grid->addWidget(savestate, row + 5, column, rowSpan, columnSpan); + grid->addWidget(captures, row + 6, column, rowSpan, columnSpan); + grid->addWidget(recordings, row + 7, column, rowSpan, columnSpan); + grid->addWidget(screenshots, row + 8, column, rowSpan, columnSpan); + + if (mb.exec() != QMessageBox::Yes) + return; + + // Remove data path in "dev_hdd0/game" folder (if any) and lock file in "dev_hdd0/game/$locks" folder (if any) + u16 content_types = DATA | LOCKS; + + // Remove serials (title id) in "games.yml" file (if any) + if (disc->isChecked()) + content_types |= DISC; + + // Remove caches in "cache" and "dev_hdd1/caches" folders (if any) and custom configs in "config/custom_config" folder (if any) + if (caches->isChecked()) + content_types |= CACHES | CUSTOM_CONFIG; + + // Remove icons in "Icons/game_icons" folder (if any) and + // shortcuts in "games/shortcuts" folder and from desktop / start menu (if any) + if (icons->isChecked()) + content_types |= ICONS | SHORTCUTS; + + if (savestate->isChecked()) + content_types |= SAVESTATES; + + if (captures->isChecked()) + content_types |= CAPTURES; + + if (recordings->isChecked()) + content_types |= RECORDINGS; + + if (screenshots->isChecked()) + content_types |= SCREENSHOTS; + + SetContentList(content_types, content_info); + + if (content_info.is_single_selection) // Single selection + { + if (!RemoveContentList(games[0]->info.serial, true)) + { + QMessageBox::critical(m_game_list_frame, tr("Failure!"), caches->isChecked() + ? tr("Failed to remove %0 from drive!\nCaches and custom configs have been left intact.").arg(QString::fromStdString(games[0]->info.name)) + : tr("Failed to remove %0 from drive!").arg(QString::fromStdString(games[0]->info.name))); + + return; + } + } + else // Multi selection + { + BatchRemoveContentLists(games, true); + } +} + +void game_list_actions::ShowGameInfoDialog(const std::vector& games) +{ + if (games.empty()) + return; + + QMessageBox::information(m_game_list_frame, tr("Game Info"), GetContentInfo(games).info); +} + +bool game_list_actions::IsGameRunning(const std::string& serial) +{ + return !Emu.IsStopped(true) && (serial == Emu.GetTitleID() || (serial == "vsh.self" && Emu.IsVsh())); +} + +bool game_list_actions::ValidateRemoval(const std::string& serial, const std::string& path, const std::string& desc, bool is_interactive) +{ + if (serial.empty()) + { + game_list_log.error("Removal of %s not allowed due to no title ID provided!", desc); + return false; + } + + if (path.empty() || !fs::exists(path) || (!fs::is_dir(path) && !fs::is_file(path))) + { + game_list_log.success("Could not find %s directory/file: %s (%s)", desc, path, serial); + return false; + } + + if (is_interactive) + { + if (IsGameRunning(serial)) + { + game_list_log.error("Removal of %s not allowed due to %s title is running!", desc, serial); + + QMessageBox::critical(m_game_list_frame, tr("Removal Aborted"), + tr("Removal of %0 not allowed due to %1 title is running!") + .arg(QString::fromStdString(desc)).arg(QString::fromStdString(serial))); + + return false; + } + + if (QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), tr("Remove %0?").arg(QString::fromStdString(desc))) != QMessageBox::Yes) + return false; + } + + return true; +} + +bool game_list_actions::ValidateBatchRemoval(const std::string& desc, bool is_interactive) +{ + if (!Emu.IsStopped(true)) + { + game_list_log.error("Removal of %s not allowed due to emulator is running!", desc); + + if (is_interactive) + { + QMessageBox::critical(m_game_list_frame, tr("Removal Aborted"), + tr("Removal of %0 not allowed due to emulator is running!").arg(QString::fromStdString(desc))); + } + + return false; + } + + if (is_interactive) + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), tr("Remove %0?").arg(QString::fromStdString(desc))) != QMessageBox::Yes) + return false; + } + + return true; +} + +bool game_list_actions::CreateCPUCaches(const std::string& path, const std::string& serial, bool is_fast_compilation) +{ + Emu.GracefulShutdown(false); + Emu.SetForceBoot(true); + Emu.SetPrecompileCacheOption(emu_precompilation_option_t{.is_fast = is_fast_compilation}); + + if (const auto error = Emu.BootGame(fs::is_file(path) ? fs::get_parent_dir(path) : path, serial, true); error != game_boot_result::no_errors) + { + game_list_log.error("Could not create LLVM caches for %s, error: %s", path, error); + return false; + } + + game_list_log.warning("Creating LLVM Caches for %s", path); + return true; +} + +bool game_list_actions::CreateCPUCaches(const game_info& game, bool is_fast_compilation) +{ + return game && CreateCPUCaches(game->info.path, game->info.serial, is_fast_compilation); +} + +bool game_list_actions::RemoveCustomConfiguration(const std::string& serial, const game_info& game, bool is_interactive) +{ + const std::string path = rpcs3::utils::get_custom_config_path(serial); + + if (!ValidateRemoval(serial, path, "custom configuration", is_interactive)) + return true; + + bool result = true; + + if (fs::is_file(path)) + { + if (fs::remove_file(path)) + { + if (game) + { + game->has_custom_config = false; + } + game_list_log.success("Removed configuration file: %s", path); + } + else + { + game_list_log.fatal("Failed to remove configuration file: %s\nError: %s", path, fs::g_tls_error); + result = false; + } + } + + if (is_interactive && !result) + { + QMessageBox::warning(m_game_list_frame, tr("Warning!"), tr("Failed to remove configuration file!")); + } + + return result; +} + +bool game_list_actions::RemoveCustomPadConfiguration(const std::string& serial, const game_info& game, bool is_interactive) +{ + const std::string config_dir = rpcs3::utils::get_input_config_dir(serial); + + if (!ValidateRemoval(serial, config_dir, "custom gamepad configuration", false)) // no interation needed here + return true; + + if (is_interactive && QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), (!Emu.IsStopped(true) && Emu.GetTitleID() == serial) + ? tr("Remove custom gamepad configuration?\nYour configuration will revert to the global pad settings.") + : tr("Remove custom gamepad configuration?")) != QMessageBox::Yes) + return true; + + g_cfg_input_configs.load(); + g_cfg_input_configs.active_configs.erase(serial); + g_cfg_input_configs.save(); + game_list_log.notice("Removed active input configuration entry for key '%s'", serial); + + if (QDir(QString::fromStdString(config_dir)).removeRecursively()) + { + if (game) + { + game->has_custom_pad_config = false; + } + if (!Emu.IsStopped(true) && Emu.GetTitleID() == serial) + { + pad::set_enabled(false); + pad::reset(serial); + pad::set_enabled(true); + } + game_list_log.notice("Removed gamepad configuration directory: %s", config_dir); + return true; + } + + if (is_interactive) + { + QMessageBox::warning(m_game_list_frame, tr("Warning!"), tr("Failed to completely remove gamepad configuration directory!")); + game_list_log.fatal("Failed to completely remove gamepad configuration directory: %s\nError: %s", config_dir, fs::g_tls_error); + } + return false; +} + +bool game_list_actions::RemoveShaderCache(const std::string& serial, bool is_interactive) +{ + const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); + + if (!ValidateRemoval(serial, base_dir, "shader cache", is_interactive)) + return true; + + u32 caches_removed = 0; + u32 caches_total = 0; + + const QStringList filter{ QStringLiteral("shaders_cache") }; + const QString q_base_dir = QString::fromStdString(base_dir); + + QDirIterator dir_iter(q_base_dir, filter, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + + if (QDir(filepath).removeRecursively()) + { + ++caches_removed; + game_list_log.notice("Removed shader cache directory: %s", filepath); + } + else + { + game_list_log.warning("Could not completely remove shader cache directory: %s", filepath); + } + + ++caches_total; + } + + const bool success = caches_total == caches_removed; + + if (success) + game_list_log.success("Removed shader cache in %s", base_dir); + else + game_list_log.fatal("Only %d/%d shader cache directories could be removed in %s", caches_removed, caches_total, base_dir); + + if (QDir(q_base_dir).isEmpty()) + { + if (fs::remove_dir(base_dir)) + game_list_log.notice("Removed empty shader cache directory: %s", base_dir); + else + game_list_log.error("Could not remove empty shader cache directory: '%s' (%s)", base_dir, fs::g_tls_error); + } + + return success; +} + +bool game_list_actions::RemovePPUCache(const std::string& serial, bool is_interactive) +{ + const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); + + if (!ValidateRemoval(serial, base_dir, "PPU cache", is_interactive)) + return true; + + u32 files_removed = 0; + u32 files_total = 0; + + const QStringList filter{ QStringLiteral("v*.obj"), QStringLiteral("v*.obj.gz") }; + const QString q_base_dir = QString::fromStdString(base_dir); + + QDirIterator dir_iter(q_base_dir, filter, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + + if (QFile::remove(filepath)) + { + ++files_removed; + game_list_log.notice("Removed PPU cache file: %s", filepath); + } + else + { + game_list_log.warning("Could not remove PPU cache file: %s", filepath); + } + + ++files_total; + } + + const bool success = files_total == files_removed; + + if (success) + game_list_log.success("Removed PPU cache in %s", base_dir); + else + game_list_log.fatal("Only %d/%d PPU cache files could be removed in %s", files_removed, files_total, base_dir); + + if (QDir(q_base_dir).isEmpty()) + { + if (fs::remove_dir(base_dir)) + game_list_log.notice("Removed empty PPU cache directory: %s", base_dir); + else + game_list_log.error("Could not remove empty PPU cache directory: '%s' (%s)", base_dir, fs::g_tls_error); + } + + return success; +} + +bool game_list_actions::RemoveSPUCache(const std::string& serial, bool is_interactive) +{ + const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); + + if (!ValidateRemoval(serial, base_dir, "SPU cache", is_interactive)) + return true; + + u32 files_removed = 0; + u32 files_total = 0; + + const QStringList filter{ QStringLiteral("spu*.dat"), QStringLiteral("spu*.dat.gz"), QStringLiteral("spu*.obj"), QStringLiteral("spu*.obj.gz") }; + const QString q_base_dir = QString::fromStdString(base_dir); + + QDirIterator dir_iter(q_base_dir, filter, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + + if (QFile::remove(filepath)) + { + ++files_removed; + game_list_log.notice("Removed SPU cache file: %s", filepath); + } + else + { + game_list_log.warning("Could not remove SPU cache file: %s", filepath); + } + + ++files_total; + } + + const bool success = files_total == files_removed; + + if (success) + game_list_log.success("Removed SPU cache in %s", base_dir); + else + game_list_log.fatal("Only %d/%d SPU cache files could be removed in %s", files_removed, files_total, base_dir); + + if (QDir(q_base_dir).isEmpty()) + { + if (fs::remove_dir(base_dir)) + game_list_log.notice("Removed empty SPU cache directory: %s", base_dir); + else + game_list_log.error("Could not remove empty SPU cache directory: '%s' (%s)", base_dir, fs::g_tls_error); + } + + return success; +} + +bool game_list_actions::RemoveHDD1Cache(const std::string& serial, bool is_interactive) +{ + const std::string base_dir = rpcs3::utils::get_hdd1_cache_dir(); + + if (!ValidateRemoval(serial, base_dir, "HDD1 cache", is_interactive)) + return true; + + u32 dirs_removed = 0; + u32 dirs_total = 0; + + const QStringList filter{ QString::fromStdString(serial + "_*") }; + const QString q_base_dir = QString::fromStdString(base_dir); + + QDirIterator dir_iter(q_base_dir, filter, QDir::Dirs | QDir::NoDotAndDotDot); + + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + + if (fs::remove_all(filepath.toStdString())) + { + ++dirs_removed; + game_list_log.notice("Removed HDD1 cache directory: %s", filepath); + } + else + { + game_list_log.warning("Could not completely remove HDD1 cache directory: %s", filepath); + } + + ++dirs_total; + } + + const bool success = dirs_removed == dirs_total; + + if (success) + game_list_log.success("Removed HDD1 cache in %s (%s)", base_dir, serial); + else + game_list_log.fatal("Only %d/%d HDD1 cache directories could be removed in %s (%s)", dirs_removed, dirs_total, base_dir, serial); + + return success; +} + +bool game_list_actions::RemoveAllCaches(const std::string& serial, bool is_interactive) +{ + // Just used for confirmation, if requested. Folder returned by fs::get_config_dir() is always present! + if (!ValidateRemoval(serial, fs::get_config_dir(), "all caches", is_interactive)) + return true; + + const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); + + if (!ValidateRemoval(serial, base_dir, "main cache", false)) // no interation needed here + return true; + + bool success = false; + + if (fs::remove_all(base_dir)) + { + success = true; + game_list_log.success("Removed main cache in %s", base_dir); + + } + else + { + game_list_log.fatal("Could not completely remove main cache in %s (%s)", base_dir, serial); + } + + success |= RemoveHDD1Cache(serial); + + return success; +} + +bool game_list_actions::RemoveContentList(const std::string& serial, bool is_interactive) +{ + // Just used for confirmation, if requested. Folder returned by fs::get_config_dir() is always present! + if (!ValidateRemoval(serial, fs::get_config_dir(), "selected content", is_interactive)) + { + if (m_content_info.clear_on_finish) + ClearContentList(); // Clear only the content's info + + return true; + } + + u16 content_types = m_content_info.content_types; + + // Remove data path in "dev_hdd0/game" folder (if any) + if (content_types & DATA) + { + if (const auto it = m_content_info.path_list.find(serial); it != m_content_info.path_list.cend()) + { + if (RemoveContentPathList(it->second, "data") != it->second.size()) + { + if (m_content_info.clear_on_finish) + ClearContentList(); // Clear only the content's info + + // Skip the removal of the remaining selected contents in case some data paths could not be removed + return false; + } + } + } + + // Add serial (title id) to the list of serials to be removed in "games.yml" file (if any) + if (content_types & DISC) + { + if (const auto it = m_content_info.disc_list.find(serial); it != m_content_info.disc_list.cend()) + m_content_info.removed_disc_list.insert(serial); + } + + // Remove lock file in "dev_hdd0/game/$locks" folder (if any) + if (content_types & LOCKS) + { + if (ValidateRemoval(serial, rpcs3::utils::get_hdd0_locks_dir(), "lock")) + RemoveContentBySerial(rpcs3::utils::get_hdd0_locks_dir(), serial, "lock"); + } + + // Remove caches in "cache" and "dev_hdd1/caches" folders (if any) + if (content_types & CACHES) + { + if (ValidateRemoval(serial, rpcs3::utils::get_cache_dir_by_serial(serial), "all caches")) + RemoveAllCaches(serial); + } + + // Remove custom configs in "config/custom_config" folder (if any) + if (content_types & CUSTOM_CONFIG) + { + if (ValidateRemoval(serial, rpcs3::utils::get_custom_config_path(serial), "custom configuration")) + RemoveCustomConfiguration(serial); + + if (ValidateRemoval(serial, rpcs3::utils::get_input_config_dir(serial), "custom gamepad configuration")) + RemoveCustomPadConfiguration(serial); + } + + // Remove icons in "Icons/game_icons" folder (if any) + if (content_types & ICONS) + { + if (ValidateRemoval(serial, rpcs3::utils::get_icons_dir(serial), "icons")) + RemoveContentBySerial(rpcs3::utils::get_icons_dir(), serial, "icons"); + } + + // Remove shortcuts in "games/shortcuts" folder and from desktop / start menu (if any) + if (content_types & SHORTCUTS) + { + if (const auto it = m_content_info.name_list.find(serial); it != m_content_info.name_list.cend()) + { + const bool remove_rpcs3_links = ValidateRemoval(serial, rpcs3::utils::get_games_shortcuts_dir(), "link"); + + for (const auto& name : it->second) + { + // Remove illegal characters from name to match the link name created by gui::utils::create_shortcut() + const std::string simple_name = QString::fromStdString(vfs::escape(name, true)).simplified().toStdString(); + + // Remove rpcs3 shortcuts + if (remove_rpcs3_links) + RemoveContentBySerial(rpcs3::utils::get_games_shortcuts_dir(), simple_name + ".lnk", "link"); + + // TODO: Remove shortcuts from desktop/start menu + } + } + } + + if (content_types & SAVESTATES) + { + if (ValidateRemoval(serial, rpcs3::utils::get_savestates_dir(serial), "savestates")) + RemoveContentBySerial(rpcs3::utils::get_savestates_dir(), serial, "savestates"); + } + + if (content_types & CAPTURES) + { + if (ValidateRemoval(serial, rpcs3::utils::get_captures_dir(), "captures")) + RemoveContentBySerial(rpcs3::utils::get_captures_dir(), serial, "captures"); + } + + if (content_types & RECORDINGS) + { + if (ValidateRemoval(serial, rpcs3::utils::get_recordings_dir(serial), "recordings")) + RemoveContentBySerial(rpcs3::utils::get_recordings_dir(), serial, "recordings"); + } + + if (content_types & SCREENSHOTS) + { + if (ValidateRemoval(serial, rpcs3::utils::get_screenshots_dir(serial), "screenshots")) + RemoveContentBySerial(rpcs3::utils::get_screenshots_dir(), serial, "screenshots"); + } + + if (m_content_info.clear_on_finish) + ClearContentList(true); // Update the game list and clear the content's info once removed + + return true; +} + +void game_list_actions::BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, + QString progressLabel, std::function action, + std::function cancel_log, std::function action_on_finish, bool refresh_on_finish, + bool can_be_concurrent, std::function should_wait_cb) +{ + // Concurrent tasks should not wait (at least not in current implementation) + ensure(!should_wait_cb || !can_be_concurrent); + + g_system_progress_canceled = false; + + const std::shared_ptr> iterate_over_serial = std::make_shared>(); + + const std::shared_ptr> index = std::make_shared>(0); + + const int serials_size = ::narrow(serials.size()); + + *iterate_over_serial = [=, this, index_ptr = index](int index) + { + if (index == serials_size) + { + return false; + } + + const std::string& serial = *std::next(serials.begin(), index); + + if (pdlg->wasCanceled() || g_system_progress_canceled.exchange(false)) + { + if (cancel_log) + { + cancel_log(index, serials_size); + } + return false; + } + + if (action(serial)) + { + const int done = index_ptr->load(); + pdlg->setLabelText(progressLabel.arg(done + 1).arg(serials_size)); + pdlg->SetValue(done + 1); + } + + (*index_ptr)++; + return true; + }; + + if (can_be_concurrent) + { + // Unused currently + + QList indices; + + for (int i = 0; i < serials_size; i++) + { + indices.append(i); + } + + QFutureWatcher* future_watcher = new QFutureWatcher(m_game_list_frame); + + future_watcher->setFuture(QtConcurrent::map(std::move(indices), *iterate_over_serial)); + + connect(future_watcher, &QFutureWatcher::finished, m_game_list_frame, [=, this]() + { + pdlg->setLabelText(progressLabel.arg(index->load()).arg(serials_size)); + pdlg->setCancelButtonText(tr("OK")); + QApplication::beep(); + + if (action_on_finish) + { + action_on_finish(); + } + + if (refresh_on_finish && index) + { + m_game_list_frame->Refresh(true); + } + + future_watcher->deleteLater(); + }); + + return; + } + + const std::shared_ptr> periodic_func = std::make_shared>(); + + *periodic_func = [=, this]() + { + if (should_wait_cb && should_wait_cb()) + { + // Conditions are not met for execution + // Check again later + QTimer::singleShot(5, m_game_list_frame, *periodic_func); + return; + } + + if ((*iterate_over_serial)(*index)) + { + QTimer::singleShot(1, m_game_list_frame, *periodic_func); + return; + } + + pdlg->setLabelText(progressLabel.arg(index->load()).arg(serials_size)); + pdlg->setCancelButtonText(tr("OK")); + connect(pdlg, &progress_dialog::canceled, m_game_list_frame, [pdlg](){ pdlg->deleteLater(); }); + QApplication::beep(); + + if (action_on_finish) + { + action_on_finish(); + } + + // Signal termination back to the callback + action(""); + + if (refresh_on_finish && index) + { + m_game_list_frame->Refresh(true); + } + }; + + // Invoked on the next event loop processing iteration + QTimer::singleShot(1, m_game_list_frame, *periodic_func); +} + +void game_list_actions::BatchCreateCPUCaches(const std::vector& games, bool is_fast_compilation, bool is_interactive) +{ + if (is_interactive && QMessageBox::question(m_game_list_frame, tr("Confirm Creation"), tr("Create LLVM cache?")) != QMessageBox::Yes) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const usz total = serials.size(); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("LLVM Cache Batch Creation"), tr("No titles found"), QMessageBox::Ok); + return; + } + + if (!m_gui_settings->GetBootConfirmation(m_game_list_frame)) + { + return; + } + + const QString main_label = tr("Creating all LLVM caches"); + + progress_dialog* pdlg = new progress_dialog(tr("LLVM Cache Batch Creation"), main_label, tr("Cancel"), 0, ::narrow(total), false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + connect(pdlg, &progress_dialog::canceled, m_game_list_frame, []() + { + if (!Emu.IsStopped()) + { + Emu.GracefulShutdown(false, true); + } + }); + + BatchActionBySerials(pdlg, serials, tr("%0\nProgress: %1/%2 caches compiled").arg(main_label), + [this, is_fast_compilation](const std::string& serial) + { + if (serial.empty()) + { + return false; + } + + if (Emu.IsStopped(true)) + { + const auto& games = m_game_list_frame->GetGameInfo(); + + const auto it = std::find_if(games.cbegin(), games.cend(), FN(x->info.serial == serial)); + + if (it != games.cend()) + { + return CreateCPUCaches((*it)->info.path, serial, is_fast_compilation); + } + } + + return false; + }, + [](u32, u32) + { + game_list_log.notice("LLVM Cache Batch Creation was canceled"); + }, nullptr, false, false, + []() + { + return !Emu.IsStopped(true); + }); +} + +void game_list_actions::BatchRemovePPUCaches(const std::vector& games, bool is_interactive) +{ + if (!ValidateBatchRemoval("PPU cache", is_interactive)) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("PPU Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("PPU Cache Batch Removal"), tr("Removing all PPU caches"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 PPU caches cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemovePPUCache(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("PPU Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); + }, nullptr, false); +} + +void game_list_actions::BatchRemoveSPUCaches(const std::vector& games, bool is_interactive) +{ + if (!ValidateBatchRemoval("SPU cache", is_interactive)) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("SPU Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("SPU Cache Batch Removal"), tr("Removing all SPU caches"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 SPU caches cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveSPUCache(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("SPU Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); + }, nullptr, false); +} + +void game_list_actions::BatchRemoveHDD1Caches(const std::vector& games, bool is_interactive) +{ + if (!ValidateBatchRemoval("HDD1 cache", is_interactive)) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("HDD1 Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("HDD1 Cache Batch Removal"), tr("Removing all HDD1 caches"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 HDD1 caches cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveHDD1Cache(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("HDD1 Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); + }, nullptr, false); +} + +void game_list_actions::BatchRemoveAllCaches(const std::vector& games, bool is_interactive) +{ + if (!ValidateBatchRemoval("all caches", is_interactive)) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("Cache Batch Removal"), tr("Removing all caches"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 caches cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveAllCaches(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); + }, nullptr, false); +} + +void game_list_actions::BatchRemoveContentLists(const std::vector& games, bool is_interactive) +{ + // Let the batch process (not RemoveContentList()) make cleanup when terminated + m_content_info.clear_on_finish = false; + + if (!ValidateBatchRemoval("selected content", is_interactive)) + { + ClearContentList(); // Clear only the content's info + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("Content Batch Removal"), tr("No files found"), QMessageBox::Ok); + + ClearContentList(); // Clear only the content's info + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("Content Batch Removal"), tr("Removing all contents"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 contents cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveContentList(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("Content Batch Removal was canceled. %d/%d contents cleared", removed, total); + }, + [this]() // Make cleanup when batch process terminated + { + ClearContentList(true); // Update the game list and clear the content's info once removed + }, false); +} + +void game_list_actions::BatchRemoveCustomConfigurations(const std::vector& games, bool is_interactive) +{ + if (is_interactive && QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), tr("Remove custom configuration?")) != QMessageBox::Yes) + { + return; + } + + std::set serials; + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + if (game->has_custom_config && !serials.count(game->info.serial)) + { + serials.emplace(game->info.serial); + } + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("Custom Configuration Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("Custom Configuration Batch Removal"), tr("Removing all custom configurations"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 custom configurations cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveCustomConfiguration(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("Custom Configuration Batch Removal was canceled. %d/%d custom configurations cleared", removed, total); + }, nullptr, true); +} + +void game_list_actions::BatchRemoveCustomPadConfigurations(const std::vector& games, bool is_interactive) +{ + if (is_interactive && QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), tr("Remove custom gamepad configuration?")) != QMessageBox::Yes) + { + return; + } + + std::set serials; + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + if (game->has_custom_pad_config && !serials.count(game->info.serial)) + { + serials.emplace(game->info.serial); + } + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("Custom Gamepad Configuration Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("Custom Gamepad Configuration Batch Removal"), tr("Removing all custom gamepad configurations"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 custom gamepad configurations cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveCustomPadConfiguration(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("Custom Gamepad Configuration Batch Removal was canceled. %d/%d custom gamepad configurations cleared", removed, total); + }, nullptr, true); +} + +void game_list_actions::BatchRemoveShaderCaches(const std::vector& games, bool is_interactive) +{ + if (!ValidateBatchRemoval("shader cache", is_interactive)) + { + return; + } + + std::set serials; + + if (games.empty()) + { + serials.emplace("vsh.self"); + } + + for (const auto& game : (games.empty() ? m_game_list_frame->GetGameInfo() : games)) + { + serials.emplace(game->info.serial); + } + + const u32 total = ::size32(serials); + + if (total == 0) + { + QMessageBox::information(m_game_list_frame, tr("Shader Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); + return; + } + + progress_dialog* pdlg = new progress_dialog(tr("Shader Cache Batch Removal"), tr("Removing all shader caches"), tr("Cancel"), 0, total, false, m_game_list_frame); + pdlg->setAutoClose(false); + pdlg->setAutoReset(false); + pdlg->open(); + + BatchActionBySerials(pdlg, serials, tr("%0/%1 shader caches cleared"), + [this](const std::string& serial) + { + return !serial.empty() && Emu.IsStopped(true) && RemoveShaderCache(serial); + }, + [](u32 removed, u32 total) + { + game_list_log.notice("Shader Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); + }, nullptr, false); +} + +void game_list_actions::CreateShortcuts(const std::vector& games, const std::set& locations) +{ + if (games.empty()) + { + game_list_log.notice("Skip creating shortcuts. No games selected."); + return; + } + + if (locations.empty()) + { + game_list_log.error("Failed to create shortcuts. No locations selected."); + return; + } + + bool success = true; + + for (const game_info& gameinfo : games) + { + std::string gameid_token_value; + + const std::string dev_flash = g_cfg_vfs.get_dev_flash(); + + if (gameinfo->info.category == "DG" && !fs::is_file(rpcs3::utils::get_hdd0_dir() + "/game/" + gameinfo->info.serial + "/USRDIR/EBOOT.BIN")) + { + const usz ps3_game_dir_pos = fs::get_parent_dir(gameinfo->info.path).size(); + std::string relative_boot_dir = gameinfo->info.path.substr(ps3_game_dir_pos); + + if (usz char_pos = relative_boot_dir.find_first_not_of(fs::delim); char_pos != umax) + { + relative_boot_dir = relative_boot_dir.substr(char_pos); + } + else + { + relative_boot_dir.clear(); + } + + if (!relative_boot_dir.empty()) + { + if (relative_boot_dir != "PS3_GAME") + { + gameid_token_value = gameinfo->info.serial + "/" + relative_boot_dir; + } + else + { + gameid_token_value = gameinfo->info.serial; + } + } + } + else + { + gameid_token_value = gameinfo->info.serial; + } + +#ifdef __linux__ + const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%%%RPCS3_VFS%%%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) + : fmt::format("--no-gui \"%%%%RPCS3_GAMEID%%%%:%s\"", gameid_token_value); +#else + const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%RPCS3_VFS%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) + : fmt::format("--no-gui \"%%RPCS3_GAMEID%%:%s\"", gameid_token_value); +#endif + const std::string target_icon_dir = fmt::format("%sIcons/game_icons/%s/", fs::get_config_dir(), gameinfo->info.serial); + + if (!fs::create_path(target_icon_dir)) + { + game_list_log.error("Failed to create shortcut path %s (%s)", QString::fromStdString(gameinfo->info.name).simplified(), target_icon_dir, fs::g_tls_error); + success = false; + continue; + } + + for (const gui::utils::shortcut_location& location : locations) + { + std::string destination; + + switch (location) + { + case gui::utils::shortcut_location::desktop: + destination = "desktop"; + break; + case gui::utils::shortcut_location::applications: + destination = "application menu"; + break; +#ifdef _WIN32 + case gui::utils::shortcut_location::rpcs3_shortcuts: + destination = "/games/shortcuts/"; + break; +#endif + } + + if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) + { + game_list_log.success("Created %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); + } + else + { + game_list_log.error("Failed to create %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); + success = false; + } + } + } + +#ifdef _WIN32 + if (locations.size() == 1 && locations.contains(gui::utils::shortcut_location::rpcs3_shortcuts)) + { + return; + } +#endif + + if (success) + { + QMessageBox::information(m_game_list_frame, tr("Success!"), tr("Successfully created shortcut(s).")); + } + else + { + QMessageBox::warning(m_game_list_frame, tr("Warning!"), tr("Failed to create one or more shortcuts!")); + } +} + +bool game_list_actions::RemoveContentPath(const std::string& path, const std::string& desc) +{ + if (!fs::exists(path)) + { + return true; + } + + if (fs::is_dir(path)) + { + if (fs::remove_all(path)) + { + game_list_log.notice("Removed '%s' directory: '%s'", desc, path); + } + else + { + game_list_log.error("Could not remove '%s' directory: '%s' (%s)", desc, path, fs::g_tls_error); + + return false; + } + } + else // If file + { + if (fs::remove_file(path)) + { + game_list_log.notice("Removed '%s' file: '%s'", desc, path); + } + else + { + game_list_log.error("Could not remove '%s' file: '%s' (%s)", desc, path, fs::g_tls_error); + + return false; + } + } + + return true; +} + +u32 game_list_actions::RemoveContentPathList(const std::set& path_list, const std::string& desc) +{ + u32 paths_removed = 0; + + for (const std::string& path : path_list) + { + if (RemoveContentPath(path, desc)) + { + paths_removed++; + } + } + + return paths_removed; +} + +bool game_list_actions::RemoveContentBySerial(const std::string& base_dir, const std::string& serial, const std::string& desc) +{ + bool success = true; + + for (const auto& entry : fs::dir(base_dir)) + { + // Search for any path starting with serial (e.g. BCES01118_BCES01118) + if (!entry.name.starts_with(serial)) + { + continue; + } + + if (!RemoveContentPath(base_dir + entry.name, desc)) + { + success = false; // Mark as failed if there is at least one failure + } + } + + return success; +} diff --git a/rpcs3/rpcs3qt/game_list_actions.h b/rpcs3/rpcs3qt/game_list_actions.h new file mode 100644 index 0000000000..7c5f603337 --- /dev/null +++ b/rpcs3/rpcs3qt/game_list_actions.h @@ -0,0 +1,113 @@ +#pragma once + +#include "gui_game_info.h" +#include "shortcut_utils.h" + +#include + +class progress_dialog; +class game_list_frame; +class gui_settings; + +class game_list_actions : QObject +{ + Q_OBJECT + +public: + game_list_actions(game_list_frame* frame, std::shared_ptr gui_settings); + virtual ~game_list_actions(); + + enum content_type + { + NO_CONTENT = 0, + DISC = (1 << 0), + DATA = (1 << 1), + LOCKS = (1 << 2), + CACHES = (1 << 3), + CUSTOM_CONFIG = (1 << 4), + ICONS = (1 << 5), + SHORTCUTS = (1 << 6), + SAVESTATES = (1 << 7), + CAPTURES = (1 << 8), + RECORDINGS = (1 << 9), + SCREENSHOTS = (1 << 10) + }; + + struct content_info + { + u16 content_types = NO_CONTENT; // Always set by SetContentList() + bool clear_on_finish = true; // Always overridden by BatchRemoveContentLists() + + bool is_single_selection = false; + u16 in_games_dir_count = 0; + QString info; + std::map> name_list; + std::map> path_list; + std::set disc_list; + std::set removed_disc_list; // Filled in by RemoveContentList() + }; + + static bool IsGameRunning(const std::string& serial); + + void CreateShortcuts(const std::vector& games, const std::set& locations); + + void ShowRemoveGameDialog(const std::vector& games); + void ShowGameInfoDialog(const std::vector& games); + + void BatchCreateCPUCaches(const std::vector& games = {}, bool is_fast_compilation = false, bool is_interactive = false); + void BatchRemoveCustomConfigurations(const std::vector& games = {}, bool is_interactive = false); + void BatchRemoveCustomPadConfigurations(const std::vector& games = {}, bool is_interactive = false); + void BatchRemoveShaderCaches(const std::vector& games = {}, bool is_interactive = false); + void BatchRemovePPUCaches(const std::vector& games = {}, bool is_interactive = false); + void BatchRemoveSPUCaches(const std::vector& games = {}, bool is_interactive = false); + void BatchRemoveHDD1Caches(const std::vector& games = {}, bool is_interactive = false); + void BatchRemoveAllCaches(const std::vector& games = {}, bool is_interactive = false); + + // NOTES: + // - SetContentList() MUST always be called to set the content's info to be removed by: + // - RemoveContentList() + // - BatchRemoveContentLists() + // + void SetContentList(u16 content_types, const content_info& content_info); + void BatchRemoveContentLists(const std::vector& games = {}, bool is_interactive = false); + + void ClearContentList(bool refresh = false); + content_info GetContentInfo(const std::vector& games); + + bool ValidateRemoval(const std::string& serial, const std::string& path, const std::string& desc, bool is_interactive = false); + bool ValidateBatchRemoval(const std::string& desc, bool is_interactive = false); + + static bool CreateCPUCaches(const std::string& path, const std::string& serial = {}, bool is_fast_compilation = false); + static bool CreateCPUCaches(const game_info& game, bool is_fast_compilation = false); + bool RemoveCustomConfiguration(const std::string& serial, const game_info& game = nullptr, bool is_interactive = false); + bool RemoveCustomPadConfiguration(const std::string& serial, const game_info& game = nullptr, bool is_interactive = false); + bool RemoveShaderCache(const std::string& serial, bool is_interactive = false); + bool RemovePPUCache(const std::string& serial, bool is_interactive = false); + bool RemoveSPUCache(const std::string& serial, bool is_interactive = false); + bool RemoveHDD1Cache(const std::string& serial, bool is_interactive = false); + bool RemoveAllCaches(const std::string& serial, bool is_interactive = false); + bool RemoveContentList(const std::string& serial, bool is_interactive = false); + + static bool RemoveContentPath(const std::string& path, const std::string& desc); + static u32 RemoveContentPathList(const std::set& path_list, const std::string& desc); + static bool RemoveContentBySerial(const std::string& base_dir, const std::string& serial, const std::string& desc); + +private: + game_list_frame* m_game_list_frame = nullptr; + std::shared_ptr m_gui_settings; + + // NOTE: + // m_content_info is used by: + // - SetContentList() + // - ClearContentList() + // - GetContentInfo() + // - RemoveContentList() + // - BatchRemoveContentLists() + // + content_info m_content_info; + + void BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, + QString progressLabel, std::function action, + std::function cancel_log, std::function action_on_finish, bool refresh_on_finish, + bool can_be_concurrent = false, std::function should_wait_cb = {}); +}; diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp new file mode 100644 index 0000000000..a04f5b0128 --- /dev/null +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -0,0 +1,907 @@ +#include "stdafx.h" +#include "game_list_context_menu.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "category.h" +#include "input_dialog.h" +#include "qt_utils.h" +#include "shortcut_utils.h" +#include "settings_dialog.h" +#include "pad_settings_dialog.h" +#include "patch_manager_dialog.h" +#include "persistent_settings.h" + +#include "Utilities/File.h" +#include "Emu/system_utils.hpp" + +#include "QApplication" +#include "QClipboard" +#include "QDesktopServices" +#include "QFileDialog" +#include "QInputDialog" +#include "QMessageBox" + +LOG_CHANNEL(game_list_log, "GameList"); +LOG_CHANNEL(sys_log, "SYS"); + +std::string get_savestate_file(std::string_view title_id, std::string_view boot_pat, s64 rel_id, u64 aggregate_file_size = umax); + +game_list_context_menu::game_list_context_menu(game_list_frame* frame) + : QMenu(frame) + , m_game_list_frame(ensure(frame)) + , m_game_list_actions(ensure(frame->actions())) + , m_gui_settings(ensure(frame->get_gui_settings())) + , m_emu_settings(ensure(frame->get_emu_settings())) + , m_persistent_settings(ensure(frame->get_persistent_settings())) +{ +} + +game_list_context_menu::~game_list_context_menu() +{ +} + +void game_list_context_menu::show_menu(const std::vector& games, const QPoint& global_pos) +{ + if (games.empty()) return; + + if (games.size() == 1) + { + show_single_selection_context_menu(games.front(), global_pos); + } + else + { + show_multi_selection_context_menu(games, global_pos); + } +} + +void game_list_context_menu::show_single_selection_context_menu(const game_info& gameinfo, const QPoint& global_pos) +{ + ensure(!!gameinfo); + + GameInfo current_game = gameinfo->info; + const std::string serial = current_game.serial; + const QString name = QString::fromStdString(current_game.name).simplified(); + const bool is_current_running_game = game_list_actions::IsGameRunning(serial); + + // Make Actions + QAction* boot = new QAction(gameinfo->has_custom_config + ? (is_current_running_game + ? tr("&Reboot with Global Configuration") + : tr("&Boot with Global Configuration")) + : (is_current_running_game + ? tr("&Reboot") + : tr("&Boot"))); + + QFont font = boot->font(); + font.setBold(true); + + if (gameinfo->has_custom_config) + { + QAction* boot_custom = addAction(is_current_running_game + ? tr("&Reboot with Custom Configuration") + : tr("&Boot with Custom Configuration")); + boot_custom->setFont(font); + connect(boot_custom, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + sys_log.notice("Booting from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo); + }); + } + else + { + boot->setFont(font); + } + + addAction(boot); + + { + QAction* boot_default = addAction(is_current_running_game + ? tr("&Reboot with Default Configuration") + : tr("&Boot with Default Configuration")); + + connect(boot_default, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + sys_log.notice("Booting from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::default_config); + }); + + QAction* boot_manual = addAction(is_current_running_game + ? tr("&Reboot with Manually Selected Configuration") + : tr("&Boot with Manually Selected Configuration")); + + connect(boot_manual, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + if (const std::string file_path = QFileDialog::getOpenFileName(m_game_list_frame, "Select Config File", "", tr("Config Files (*.yml);;All files (*.*)")).toStdString(); !file_path.empty()) + { + sys_log.notice("Booting from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::custom_selection, file_path); + } + else + { + sys_log.notice("Manual config selection aborted."); + } + }); + } + + extern bool is_savestate_compatible(const std::string& filepath); + + if (const std::string sstate = get_savestate_file(serial, current_game.path, 1); is_savestate_compatible(sstate)) + { + const bool has_ambiguity = !get_savestate_file(serial, current_game.path, 2).empty(); + + QAction* boot_state = addAction(is_current_running_game + ? tr("&Reboot with last SaveState") + : tr("&Boot with last SaveState")); + + connect(boot_state, &QAction::triggered, m_game_list_frame, [this, gameinfo, sstate] + { + sys_log.notice("Booting savestate from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::custom, "", sstate); + }); + + if (has_ambiguity) + { + QAction* choose_state = addAction(is_current_running_game + ? tr("&Choose SaveState to reboot") + : tr("&Choose SaveState to boot")); + + connect(choose_state, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + // If there is any ambiguity, launch the savestate manager + Q_EMIT m_game_list_frame->RequestSaveStateManager(gameinfo); + }); + } + } + + addSeparator(); + + QAction* configure = addAction(gameinfo->has_custom_config + ? tr("&Change Custom Configuration") + : tr("&Create Custom Configuration From Global Settings")); + QAction* create_game_default_config = gameinfo->has_custom_config ? nullptr + : addAction(tr("&Create Custom Configuration From Default Settings")); + QAction* pad_configure = addAction(gameinfo->has_custom_pad_config + ? tr("&Change Custom Gamepad Configuration") + : tr("&Create Custom Gamepad Configuration")); + QAction* configure_patches = addAction(tr("&Manage Game Patches")); + + addSeparator(); + + // Create LLVM cache + QAction* create_cpu_cache = addAction(tr("&Create LLVM Cache")); + + // Remove menu + QMenu* remove_menu = addMenu(tr("&Remove")); + + if (gameinfo->has_custom_config) + { + QAction* remove_custom_config = remove_menu->addAction(tr("&Remove Custom Configuration")); + connect(remove_custom_config, &QAction::triggered, this, [this, serial, gameinfo]() + { + if (m_game_list_actions->RemoveCustomConfiguration(serial, gameinfo, true)) + { + m_game_list_frame->ShowCustomConfigIcon(gameinfo); + } + }); + } + if (gameinfo->has_custom_pad_config) + { + QAction* remove_custom_pad_config = remove_menu->addAction(tr("&Remove Custom Gamepad Configuration")); + connect(remove_custom_pad_config, &QAction::triggered, this, [this, serial, gameinfo]() + { + if (m_game_list_actions->RemoveCustomPadConfiguration(serial, gameinfo, true)) + { + m_game_list_frame->ShowCustomConfigIcon(gameinfo); + } + }); + } + + const std::string cache_base_dir = fs::get_path_if_dir(rpcs3::utils::get_cache_dir_by_serial(serial)); + const bool has_hdd1_cache_dir = !rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd1_cache_dir(), serial).empty(); + const std::string savestates_dir = fs::get_path_if_dir(rpcs3::utils::get_savestates_dir(serial)); + + if (!cache_base_dir.empty()) + { + remove_menu->addSeparator(); + + QAction* remove_shader_cache = remove_menu->addAction(tr("&Remove Shader Cache")); + remove_shader_cache->setEnabled(!is_current_running_game); + connect(remove_shader_cache, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->RemoveShaderCache(serial, true); + }); + + QAction* remove_ppu_cache = remove_menu->addAction(tr("&Remove PPU Cache")); + remove_ppu_cache->setEnabled(!is_current_running_game); + connect(remove_ppu_cache, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->RemovePPUCache(serial, true); + }); + + QAction* remove_spu_cache = remove_menu->addAction(tr("&Remove SPU Cache")); + remove_spu_cache->setEnabled(!is_current_running_game); + connect(remove_spu_cache, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->RemoveSPUCache(serial, true); + }); + } + + if (has_hdd1_cache_dir) + { + QAction* remove_hdd1_cache = remove_menu->addAction(tr("&Remove HDD1 Cache")); + remove_hdd1_cache->setEnabled(!is_current_running_game); + connect(remove_hdd1_cache, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->RemoveHDD1Cache(serial, true); + }); + } + + if (!cache_base_dir.empty() || has_hdd1_cache_dir) + { + QAction* remove_all_caches = remove_menu->addAction(tr("&Remove All Caches")); + remove_all_caches->setEnabled(!is_current_running_game); + connect(remove_all_caches, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->RemoveAllCaches(serial, true); + }); + } + + if (!savestates_dir.empty()) + { + remove_menu->addSeparator(); + + QAction* remove_savestates = remove_menu->addAction(tr("&Remove Savestates")); + remove_savestates->setEnabled(!is_current_running_game); + connect(remove_savestates, &QAction::triggered, this, [this, serial]() + { + m_game_list_actions->SetContentList(game_list_actions::content_type::SAVESTATES, {}); + m_game_list_actions->RemoveContentList(serial, true); + }); + } + + // Disable the Remove menu if empty + remove_menu->setEnabled(!remove_menu->isEmpty()); + + addSeparator(); + + // Manage Game menu + QMenu* manage_game_menu = addMenu(tr("&Manage Game")); + + // Create game shortcuts + QAction* create_desktop_shortcut = manage_game_menu->addAction(tr("&Create Desktop Shortcut")); + connect(create_desktop_shortcut, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::desktop}); + }); +#ifdef _WIN32 + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Start Menu Shortcut")); +#elif defined(__APPLE__) + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Launchpad Shortcut")); +#else + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Application Menu Shortcut")); +#endif + connect(create_start_menu_shortcut, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::applications}); + }); + + manage_game_menu->addSeparator(); + + // Hide/rename game in game list + QAction* hide_serial = manage_game_menu->addAction(tr("&Hide In Game List")); + hide_serial->setCheckable(true); + hide_serial->setChecked(m_game_list_frame->hidden_list().contains(QString::fromStdString(serial))); + QAction* rename_title = manage_game_menu->addAction(tr("&Rename In Game List")); + + // Edit tooltip notes/reset time played + QAction* edit_notes = manage_game_menu->addAction(tr("&Edit Tooltip Notes")); + QAction* reset_time_played = manage_game_menu->addAction(tr("&Reset Time Played")); + + manage_game_menu->addSeparator(); + + // Remove game + QAction* remove_game = manage_game_menu->addAction(tr("&Remove %1").arg(gameinfo->localized_category)); + remove_game->setEnabled(!is_current_running_game); + + // Game info + QAction* game_info = manage_game_menu->addAction(tr("&Game Info")); + connect(game_info, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->ShowGameInfoDialog({gameinfo}); + }); + + // Custom Images menu + QMenu* icon_menu = addMenu(tr("&Custom Images")); + const std::array custom_icon_actions = + { + icon_menu->addAction(tr("&Import Custom Icon")), + icon_menu->addAction(tr("&Replace Custom Icon")), + icon_menu->addAction(tr("&Remove Custom Icon")) + }; + icon_menu->addSeparator(); + const std::array custom_gif_actions = + { + icon_menu->addAction(tr("&Import Hover Gif")), + icon_menu->addAction(tr("&Replace Hover Gif")), + icon_menu->addAction(tr("&Remove Hover Gif")) + }; + icon_menu->addSeparator(); + const std::array custom_shader_icon_actions = + { + icon_menu->addAction(tr("&Import Custom Shader Loading Background")), + icon_menu->addAction(tr("&Replace Custom Shader Loading Background")), + icon_menu->addAction(tr("&Remove Custom Shader Loading Background")) + }; + + if (const std::string custom_icon_dir_path = rpcs3::utils::get_icons_dir(serial); + fs::create_path(custom_icon_dir_path)) + { + enum class icon_action + { + add, + replace, + remove + }; + enum class icon_type + { + game_list, + hover_gif, + shader_load + }; + + const auto handle_icon = [this, serial](const QString& game_icon_path, const QString& suffix, icon_action action, icon_type type) + { + QString icon_path; + + if (action != icon_action::remove) + { + QString msg; + switch (type) + { + case icon_type::game_list: + msg = tr("Select Custom Icon"); + break; + case icon_type::hover_gif: + msg = tr("Select Custom Hover Gif"); + break; + case icon_type::shader_load: + msg = tr("Select Custom Shader Loading Background"); + break; + } + icon_path = QFileDialog::getOpenFileName(m_game_list_frame, msg, "", tr("%0 (*.%0);;All files (*.*)").arg(suffix)); + } + if (action == icon_action::remove || !icon_path.isEmpty()) + { + bool refresh = false; + + QString msg; + switch (type) + { + case icon_type::game_list: + msg = tr("Remove Custom Icon of %0?").arg(QString::fromStdString(serial)); + break; + case icon_type::hover_gif: + msg = tr("Remove Custom Hover Gif of %0?").arg(QString::fromStdString(serial)); + break; + case icon_type::shader_load: + msg = tr("Remove Custom Shader Loading Background of %0?").arg(QString::fromStdString(serial)); + break; + } + + if (action == icon_action::replace || (action == icon_action::remove && + QMessageBox::question(m_game_list_frame, tr("Confirm Removal"), msg) == QMessageBox::Yes)) + { + if (QFile file(game_icon_path); file.exists() && !file.remove()) + { + game_list_log.error("Could not remove old file: '%s'", game_icon_path, file.errorString()); + QMessageBox::warning(m_game_list_frame, tr("Warning!"), tr("Failed to remove the old file!")); + return; + } + + game_list_log.success("Removed file: '%s'", game_icon_path); + if (action == icon_action::remove) + { + refresh = true; + } + } + + if (action != icon_action::remove) + { + if (!QFile::copy(icon_path, game_icon_path)) + { + game_list_log.error("Could not import file '%s' to '%s'.", icon_path, game_icon_path); + QMessageBox::warning(m_game_list_frame, tr("Warning!"), tr("Failed to import the new file!")); + } + else + { + game_list_log.success("Imported file '%s' to '%s'", icon_path, game_icon_path); + refresh = true; + } + } + + if (refresh) + { + m_game_list_frame->Refresh(true); + } + } + }; + + const std::vector&>> icon_map = + { + {icon_type::game_list, "/ICON0.PNG", "png", custom_icon_actions}, + {icon_type::hover_gif, "/hover.gif", "gif", custom_gif_actions}, + {icon_type::shader_load, "/PIC1.PNG", "png", custom_shader_icon_actions}, + }; + + for (const auto& [type, icon_name, suffix, actions] : icon_map) + { + const QString icon_path = QString::fromStdString(custom_icon_dir_path) + icon_name; + + if (QFile::exists(icon_path)) + { + actions[static_cast(icon_action::add)]->setVisible(false); + connect(actions[static_cast(icon_action::replace)], &QAction::triggered, m_game_list_frame, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::replace, t); }); + connect(actions[static_cast(icon_action::remove)], &QAction::triggered, m_game_list_frame, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::remove, t); }); + } + else + { + connect(actions[static_cast(icon_action::add)], &QAction::triggered, m_game_list_frame, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::add, t); }); + actions[static_cast(icon_action::replace)]->setVisible(false); + actions[static_cast(icon_action::remove)]->setEnabled(false); + } + } + } + else + { + game_list_log.error("Could not create path '%s'", custom_icon_dir_path); + icon_menu->setEnabled(false); + } + + addSeparator(); + + // Open Folder menu + QMenu* open_folder_menu = addMenu(tr("&Open Folder")); + + const bool is_disc_game = QString::fromStdString(current_game.category) == cat::cat_disc_game; + const std::string data_dir = fs::get_path_if_dir(rpcs3::utils::get_data_dir(serial)); + const std::string captures_dir = fs::get_path_if_dir(rpcs3::utils::get_captures_dir()); + const std::string recordings_dir = fs::get_path_if_dir(rpcs3::utils::get_recordings_dir(serial)); + const std::string screenshots_dir = fs::get_path_if_dir(rpcs3::utils::get_screenshots_dir(serial)); + std::set data_dir_list; + + if (is_disc_game) + { + QAction* open_disc_game_folder = open_folder_menu->addAction(tr("&Open Disc Game Folder")); + connect(open_disc_game_folder, &QAction::triggered, this, [current_game]() + { + gui::utils::open_dir(current_game.path); + }); + + // It could be an empty list for a disc game + data_dir_list = rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd0_game_dir(), serial); + } + else + { + data_dir_list.insert(current_game.path); + } + + if (!data_dir_list.empty()) // "true" if a path is present (it could be an empty list for a disc game) + { + QAction* open_data_folder = open_folder_menu->addAction(tr("&Open %0 Folder").arg(is_disc_game ? tr("Game Data") : gameinfo->localized_category)); + connect(open_data_folder, &QAction::triggered, this, [data_dir_list]() + { + for (const std::string& data_dir : data_dir_list) + { + gui::utils::open_dir(data_dir); + } + }); + } + + if (gameinfo->has_custom_config) + { + QAction* open_config_folder = open_folder_menu->addAction(tr("&Open Custom Config Folder")); + connect(open_config_folder, &QAction::triggered, this, [serial]() + { + const std::string config_path = rpcs3::utils::get_custom_config_path(serial); + + if (fs::is_file(config_path)) + gui::utils::open_dir(config_path); + }); + } + + // This is a debug feature, let's hide it by reusing debug tab protection + if (m_gui_settings->GetValue(gui::m_showDebugTab).toBool() && !cache_base_dir.empty()) + { + QAction* open_cache_folder = open_folder_menu->addAction(tr("&Open Cache Folder")); + connect(open_cache_folder, &QAction::triggered, this, [cache_base_dir]() + { + gui::utils::open_dir(cache_base_dir); + }); + } + + if (!data_dir.empty()) + { + QAction* open_data_folder = open_folder_menu->addAction(tr("&Open Data Folder")); + connect(open_data_folder, &QAction::triggered, this, [data_dir]() + { + gui::utils::open_dir(data_dir); + }); + } + + if (!savestates_dir.empty()) + { + QAction* open_savestates_folder = open_folder_menu->addAction(tr("&Open Savestates Folder")); + connect(open_savestates_folder, &QAction::triggered, this, [savestates_dir]() + { + gui::utils::open_dir(savestates_dir); + }); + } + + if (!captures_dir.empty()) + { + QAction* open_captures_folder = open_folder_menu->addAction(tr("&Open Captures Folder")); + connect(open_captures_folder, &QAction::triggered, this, [captures_dir]() + { + gui::utils::open_dir(captures_dir); + }); + } + + if (!recordings_dir.empty()) + { + QAction* open_recordings_folder = open_folder_menu->addAction(tr("&Open Recordings Folder")); + connect(open_recordings_folder, &QAction::triggered, this, [recordings_dir]() + { + gui::utils::open_dir(recordings_dir); + }); + } + + if (!screenshots_dir.empty()) + { + QAction* open_screenshots_folder = open_folder_menu->addAction(tr("&Open Screenshots Folder")); + connect(open_screenshots_folder, &QAction::triggered, this, [screenshots_dir]() + { + gui::utils::open_dir(screenshots_dir); + }); + } + + // Copy Info menu + QMenu* info_menu = addMenu(tr("&Copy Info")); + QAction* copy_info = info_menu->addAction(tr("&Copy Name + Serial")); + QAction* copy_name = info_menu->addAction(tr("&Copy Name")); + QAction* copy_serial = info_menu->addAction(tr("&Copy Serial")); + + addSeparator(); + + QAction* check_compat = addAction(tr("&Check Game Compatibility")); + QAction* download_compat = addAction(tr("&Download Compatibility Database")); + + connect(boot, &QAction::triggered, m_game_list_frame, [this, gameinfo]() + { + sys_log.notice("Booting from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::global); + }); + + const auto configure_l = [this, current_game, gameinfo](bool create_cfg_from_global_cfg) + { + settings_dialog dlg(m_gui_settings, m_emu_settings, 0, m_game_list_frame, ¤t_game, create_cfg_from_global_cfg); + + connect(&dlg, &settings_dialog::EmuSettingsApplied, [this, gameinfo]() + { + if (!gameinfo->has_custom_config) + { + gameinfo->has_custom_config = true; + m_game_list_frame->ShowCustomConfigIcon(gameinfo); + } + Q_EMIT m_game_list_frame->NotifyEmuSettingsChange(); + }); + + dlg.exec(); + }; + + if (create_game_default_config) + { + connect(configure, &QAction::triggered, m_game_list_frame, [configure_l]() { configure_l(true); }); + connect(create_game_default_config, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(false); }); + } + else + { + connect(configure, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(true); }); + } + + connect(pad_configure, &QAction::triggered, m_game_list_frame, [this, current_game, gameinfo]() + { + pad_settings_dialog dlg(m_gui_settings, m_game_list_frame, ¤t_game); + + if (dlg.exec() == QDialog::Accepted && !gameinfo->has_custom_pad_config) + { + gameinfo->has_custom_pad_config = true; + m_game_list_frame->ShowCustomConfigIcon(gameinfo); + } + }); + connect(hide_serial, &QAction::triggered, m_game_list_frame, [this, serial = QString::fromStdString(serial)](bool checked) + { + if (checked) + m_game_list_frame->hidden_list().insert(serial); + else + m_game_list_frame->hidden_list().remove(serial); + + m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_game_list_frame->hidden_list().values())); + m_game_list_frame->Refresh(); + }); + connect(create_cpu_cache, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + if (m_gui_settings->GetBootConfirmation(m_game_list_frame)) + { + m_game_list_actions->CreateCPUCaches(gameinfo); + } + }); + connect(remove_game, &QAction::triggered, this, [this, gameinfo] + { + m_game_list_actions->ShowRemoveGameDialog({gameinfo}); + }); + connect(configure_patches, &QAction::triggered, m_game_list_frame, [this, gameinfo]() + { + patch_manager_dialog patch_manager(m_gui_settings, m_game_list_frame->GetGameInfo(), gameinfo->info.serial, gameinfo->GetGameVersion(), m_game_list_frame); + patch_manager.exec(); + }); + connect(check_compat, &QAction::triggered, this, [serial = QString::fromStdString(serial)] + { + const QString link = "https://rpcs3.net/compatibility?g=" + serial; + QDesktopServices::openUrl(QUrl(link)); + }); + connect(download_compat, &QAction::triggered, m_game_list_frame, [this] + { + ensure(m_game_list_frame->GetGameCompatibility())->RequestCompatibility(true); + }); + connect(rename_title, &QAction::triggered, m_game_list_frame, [this, name, serial = QString::fromStdString(serial), global_pos] + { + const QString custom_title = m_persistent_settings->GetValue(gui::persistent::titles, serial, "").toString(); + const QString old_title = custom_title.isEmpty() ? name : custom_title; + + input_dialog dlg(128, old_title, tr("Rename Title"), tr("%0\n%1\n\nYou can clear the line in order to use the original title.").arg(name).arg(serial), name, m_game_list_frame); + dlg.move(global_pos); + + if (dlg.exec() == QDialog::Accepted) + { + const QString new_title = dlg.get_input_text().simplified(); + + if (new_title.isEmpty() || new_title == name) + { + m_game_list_frame->titles().erase(serial); + m_persistent_settings->RemoveValue(gui::persistent::titles, serial); + } + else + { + m_game_list_frame->titles().insert_or_assign(serial, new_title); + m_persistent_settings->SetValue(gui::persistent::titles, serial, new_title); + } + m_game_list_frame->Refresh(true); // full refresh in order to reliably sort the list + } + }); + connect(edit_notes, &QAction::triggered, m_game_list_frame, [this, name, serial = QString::fromStdString(serial)] + { + bool accepted = false; + const QString old_notes = m_persistent_settings->GetValue(gui::persistent::notes, serial, "").toString(); + const QString new_notes = QInputDialog::getMultiLineText(m_game_list_frame, tr("Edit Tooltip Notes"), tr("%0\n%1").arg(name).arg(serial), old_notes, &accepted); + + if (accepted) + { + if (new_notes.simplified().isEmpty()) + { + m_game_list_frame->notes().erase(serial); + m_persistent_settings->RemoveValue(gui::persistent::notes, serial); + } + else + { + m_game_list_frame->notes().insert_or_assign(serial, new_notes); + m_persistent_settings->SetValue(gui::persistent::notes, serial, new_notes); + } + m_game_list_frame->Refresh(); + } + }); + connect(reset_time_played, &QAction::triggered, m_game_list_frame, [this, name, serial = QString::fromStdString(serial)] + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Reset"), tr("Reset time played?\n\n%0 [%1]").arg(name).arg(serial)) == QMessageBox::Yes) + { + m_persistent_settings->SetPlaytime(serial, 0, false); + m_persistent_settings->SetLastPlayed(serial, 0, true); + m_game_list_frame->Refresh(); + } + }); + connect(copy_info, &QAction::triggered, this, [name, serial = QString::fromStdString(serial)] + { + QApplication::clipboard()->setText(name % QStringLiteral(" [") % serial % QStringLiteral("]")); + }); + connect(copy_name, &QAction::triggered, this, [name] + { + QApplication::clipboard()->setText(name); + }); + connect(copy_serial, &QAction::triggered, this, [serial = QString::fromStdString(serial)] + { + QApplication::clipboard()->setText(serial); + }); + + // Disable options depending on software category + const QString category = QString::fromStdString(current_game.category); + + if (category == cat::cat_ps3_os) + { + remove_game->setEnabled(false); + } + else if (category != cat::cat_disc_game && category != cat::cat_hdd_game) + { + check_compat->setEnabled(false); + } + + exec(global_pos); +} + +void game_list_context_menu::show_multi_selection_context_menu(const std::vector& games, const QPoint& global_pos) +{ + ensure(!games.empty()); + + // Create LLVM cache + QAction* create_cpu_cache = addAction(tr("&Create LLVM Cache")); + connect(create_cpu_cache, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchCreateCPUCaches(games, false, true); + }); + + // Remove menu + QMenu* remove_menu = addMenu(tr("&Remove")); + + QAction* remove_custom_config = remove_menu->addAction(tr("&Remove Custom Configuration")); + connect(remove_custom_config, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveCustomConfigurations(games, true); + }); + + QAction* remove_custom_pad_config = remove_menu->addAction(tr("&Remove Custom Gamepad Configuration")); + connect(remove_custom_pad_config, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveCustomPadConfigurations(games, true); + }); + + remove_menu->addSeparator(); + + QAction* remove_shader_cache = remove_menu->addAction(tr("&Remove Shader Cache")); + connect(remove_shader_cache, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveShaderCaches(games, true); + }); + + QAction* remove_ppu_cache = remove_menu->addAction(tr("&Remove PPU Cache")); + connect(remove_ppu_cache, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemovePPUCaches(games, true); + }); + + QAction* remove_spu_cache = remove_menu->addAction(tr("&Remove SPU Cache")); + connect(remove_spu_cache, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveSPUCaches(games, true); + }); + + QAction* remove_hdd1_cache = remove_menu->addAction(tr("&Remove HDD1 Cache")); + connect(remove_hdd1_cache, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveHDD1Caches(games, true); + }); + + QAction* remove_all_caches = remove_menu->addAction(tr("&Remove All Caches")); + connect(remove_all_caches, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->BatchRemoveAllCaches(games, true); + }); + + remove_menu->addSeparator(); + + QAction* remove_savestates = remove_menu->addAction(tr("&Remove Savestates")); + connect(remove_savestates, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->SetContentList(game_list_actions::content_type::SAVESTATES, {}); + m_game_list_actions->BatchRemoveContentLists(games, true); + }); + + // Disable the Remove menu if empty + remove_menu->setEnabled(!remove_menu->isEmpty()); + + addSeparator(); + + // Manage Game menu + QMenu* manage_game_menu = addMenu(tr("&Manage Game")); + + // Create game shortcuts + QAction* create_desktop_shortcut = manage_game_menu->addAction(tr("&Create Desktop Shortcut")); + connect(create_desktop_shortcut, &QAction::triggered, m_game_list_frame, [this, games]() + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Creation"), tr("Create desktop shortcut?")) != QMessageBox::Yes) + return; + + m_game_list_actions->CreateShortcuts(games, {gui::utils::shortcut_location::desktop}); + }); + +#ifdef _WIN32 + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Start Menu Shortcut")); +#elif defined(__APPLE__) + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Launchpad Shortcut")); +#else + QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Application Menu Shortcut")); +#endif + connect(create_start_menu_shortcut, &QAction::triggered, m_game_list_frame, [this, games]() + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Creation"), tr("Create shortcut?")) != QMessageBox::Yes) + return; + + m_game_list_actions->CreateShortcuts(games, {gui::utils::shortcut_location::applications}); + }); + + manage_game_menu->addSeparator(); + + // Hide game in game list + QAction* hide_serial = manage_game_menu->addAction(tr("&Hide In Game List")); + connect(hide_serial, &QAction::triggered, m_game_list_frame, [this, games]() + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Hiding"), tr("Hide in game list?")) != QMessageBox::Yes) + return; + + for (const auto& game : games) + { + m_game_list_frame->hidden_list().insert(QString::fromStdString(game->info.serial)); + } + + m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_game_list_frame->hidden_list().values())); + m_game_list_frame->Refresh(); + }); + + // Show game in game list + QAction* show_serial = manage_game_menu->addAction(tr("&Show In Game List")); + connect(show_serial, &QAction::triggered, m_game_list_frame, [this, games]() + { + for (const auto& game : games) + { + m_game_list_frame->hidden_list().remove(QString::fromStdString(game->info.serial)); + } + + m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_game_list_frame->hidden_list().values())); + m_game_list_frame->Refresh(); + }); + + manage_game_menu->addSeparator(); + + // Reset time played + QAction* reset_time_played = manage_game_menu->addAction(tr("&Reset Time Played")); + connect(reset_time_played, &QAction::triggered, m_game_list_frame, [this, games]() + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Reset"), tr("Reset time played?")) != QMessageBox::Yes) + return; + + for (const auto& game : games) + { + const auto serial = QString::fromStdString(game->info.serial); + + m_persistent_settings->SetPlaytime(serial, 0, false); + m_persistent_settings->SetLastPlayed(serial, 0, true); + } + + m_game_list_frame->Refresh(); + }); + + manage_game_menu->addSeparator(); + + // Remove game + QAction* remove_game = manage_game_menu->addAction(tr("&Remove Game")); + connect(remove_game, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->ShowRemoveGameDialog(games); + }); + + // Game info + QAction* game_info = manage_game_menu->addAction(tr("&Game Info")); + connect(game_info, &QAction::triggered, this, [this, games]() + { + m_game_list_actions->ShowGameInfoDialog(games); + }); + + exec(global_pos); +} diff --git a/rpcs3/rpcs3qt/game_list_context_menu.h b/rpcs3/rpcs3qt/game_list_context_menu.h new file mode 100644 index 0000000000..9da7d62dcf --- /dev/null +++ b/rpcs3/rpcs3qt/game_list_context_menu.h @@ -0,0 +1,32 @@ +#pragma once + +#include "gui_game_info.h" + +#include "QMenu" + +class game_list_actions; +class game_list_frame; +class gui_settings; +class emu_settings; +class persistent_settings; + +class game_list_context_menu : QMenu +{ + Q_OBJECT + +public: + game_list_context_menu(game_list_frame* frame); + virtual ~game_list_context_menu(); + + void show_menu(const std::vector& games, const QPoint& global_pos); + +private: + void show_single_selection_context_menu(const game_info& gameinfo, const QPoint& global_pos); + void show_multi_selection_context_menu(const std::vector& games, const QPoint& global_pos); + + game_list_frame* m_game_list_frame = nullptr; + std::shared_ptr m_game_list_actions; + std::shared_ptr m_gui_settings; + std::shared_ptr m_emu_settings; + std::shared_ptr m_persistent_settings; +}; diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 55703912b5..6910012fc3 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -1,8 +1,6 @@ #include "game_list_frame.h" +#include "game_list_context_menu.h" #include "qt_utils.h" -#include "settings_dialog.h" -#include "pad_settings_dialog.h" -#include "input_dialog.h" #include "localized.h" #include "progress_dialog.h" #include "persistent_settings.h" @@ -12,49 +10,37 @@ #include "game_list_table.h" #include "game_list_grid.h" #include "game_list_grid_item.h" -#include "patch_manager_dialog.h" #include "Emu/System.h" #include "Emu/vfs_config.h" -#include "Emu/VFS.h" #include "Emu/system_utils.hpp" #include "Loader/PSF.h" #include "util/types.hpp" #include "Utilities/File.h" #include "util/sysinfo.hpp" -#include "Input/pad_thread.h" #include #include -#include #include -#include #include #include -#include #include -#include #include #include -#include #include -#include -#include LOG_CHANNEL(game_list_log, "GameList"); LOG_CHANNEL(sys_log, "SYS"); -extern atomic_t g_system_progress_canceled; - -std::string get_savestate_file(std::string_view title_id, std::string_view boot_pat, s64 rel_id, u64 aggregate_file_size = umax); - game_list_frame::game_list_frame(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent) : custom_dock_widget(tr("Game List"), parent) , m_gui_settings(std::move(gui_settings)) , m_emu_settings(std::move(emu_settings)) , m_persistent_settings(std::move(persistent_settings)) { + m_game_list_actions = std::make_shared(this, m_gui_settings); + m_icon_size = gui::gl_icon_size_min; // ensure a valid size m_is_list_layout = m_gui_settings->GetValue(gui::gl_listMode).toBool(); m_margin_factor = m_gui_settings->GetValue(gui::gl_marginFactor).toReal(); @@ -326,79 +312,6 @@ bool game_list_frame::IsEntryVisible(const game_info& game, bool search_fallback return is_visible && matches_category() && SearchMatchesApp(QString::fromStdString(game->info.name), serial, search_fallback); } -bool game_list_frame::RemoveContentPath(const std::string& path, const std::string& desc) -{ - if (!fs::exists(path)) - { - return true; - } - - if (fs::is_dir(path)) - { - if (fs::remove_all(path)) - { - game_list_log.notice("Removed '%s' directory: '%s'", desc, path); - } - else - { - game_list_log.error("Could not remove '%s' directory: '%s' (%s)", desc, path, fs::g_tls_error); - - return false; - } - } - else // If file - { - if (fs::remove_file(path)) - { - game_list_log.notice("Removed '%s' file: '%s'", desc, path); - } - else - { - game_list_log.error("Could not remove '%s' file: '%s' (%s)", desc, path, fs::g_tls_error); - - return false; - } - } - - return true; -} - -u32 game_list_frame::RemoveContentPathList(const std::set& path_list, const std::string& desc) -{ - u32 paths_removed = 0; - - for (const std::string& path : path_list) - { - if (RemoveContentPath(path, desc)) - { - paths_removed++; - } - } - - return paths_removed; -} - -bool game_list_frame::RemoveContentBySerial(const std::string& base_dir, const std::string& serial, const std::string& desc) -{ - bool success = true; - - for (const auto& entry : fs::dir(base_dir)) - { - // Search for any path starting with serial (e.g. BCES01118_BCES01118) - if (!entry.name.starts_with(serial)) - { - continue; - } - - if (!RemoveContentPath(base_dir + entry.name, desc)) - { - success = false; // Mark as failed if there is at least one failure - } - } - - return success; -} - void game_list_frame::push_path(const std::string& path, std::vector& legit_paths) { { @@ -1043,976 +956,6 @@ void game_list_frame::ItemSelectionChangedSlot() Q_EMIT NotifyGameSelection(game); } -void game_list_frame::CreateShortcuts(const std::vector& games, const std::set& locations) -{ - if (games.empty()) - { - game_list_log.notice("Skip creating shortcuts. No games selected."); - return; - } - - if (locations.empty()) - { - game_list_log.error("Failed to create shortcuts. No locations selected."); - return; - } - - bool success = true; - - for (const game_info& gameinfo : games) - { - std::string gameid_token_value; - - const std::string dev_flash = g_cfg_vfs.get_dev_flash(); - - if (gameinfo->info.category == "DG" && !fs::is_file(rpcs3::utils::get_hdd0_dir() + "/game/" + gameinfo->info.serial + "/USRDIR/EBOOT.BIN")) - { - const usz ps3_game_dir_pos = fs::get_parent_dir(gameinfo->info.path).size(); - std::string relative_boot_dir = gameinfo->info.path.substr(ps3_game_dir_pos); - - if (usz char_pos = relative_boot_dir.find_first_not_of(fs::delim); char_pos != umax) - { - relative_boot_dir = relative_boot_dir.substr(char_pos); - } - else - { - relative_boot_dir.clear(); - } - - if (!relative_boot_dir.empty()) - { - if (relative_boot_dir != "PS3_GAME") - { - gameid_token_value = gameinfo->info.serial + "/" + relative_boot_dir; - } - else - { - gameid_token_value = gameinfo->info.serial; - } - } - } - else - { - gameid_token_value = gameinfo->info.serial; - } - -#ifdef __linux__ - const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%%%RPCS3_VFS%%%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) - : fmt::format("--no-gui \"%%%%RPCS3_GAMEID%%%%:%s\"", gameid_token_value); -#else - const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%RPCS3_VFS%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) - : fmt::format("--no-gui \"%%RPCS3_GAMEID%%:%s\"", gameid_token_value); -#endif - const std::string target_icon_dir = fmt::format("%sIcons/game_icons/%s/", fs::get_config_dir(), gameinfo->info.serial); - - if (!fs::create_path(target_icon_dir)) - { - game_list_log.error("Failed to create shortcut path %s (%s)", QString::fromStdString(gameinfo->info.name).simplified(), target_icon_dir, fs::g_tls_error); - success = false; - continue; - } - - for (const gui::utils::shortcut_location& location : locations) - { - std::string destination; - - switch (location) - { - case gui::utils::shortcut_location::desktop: - destination = "desktop"; - break; - case gui::utils::shortcut_location::applications: - destination = "application menu"; - break; -#ifdef _WIN32 - case gui::utils::shortcut_location::rpcs3_shortcuts: - destination = "/games/shortcuts/"; - break; -#endif - } - - if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) - { - game_list_log.success("Created %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); - } - else - { - game_list_log.error("Failed to create %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); - success = false; - } - } - } - -#ifdef _WIN32 - if (locations.size() == 1 && locations.contains(gui::utils::shortcut_location::rpcs3_shortcuts)) - { - return; - } -#endif - - if (success) - { - QMessageBox::information(this, tr("Success!"), tr("Successfully created shortcut(s).")); - } - else - { - QMessageBox::warning(this, tr("Warning!"), tr("Failed to create one or more shortcuts!")); - } -} - -void game_list_frame::ShowSingleSelectionContextMenu(const game_info& gameinfo, QPoint& global_pos) -{ - GameInfo current_game = gameinfo->info; - const std::string serial = current_game.serial; - const QString name = QString::fromStdString(current_game.name).simplified(); - const bool is_current_running_game = IsGameRunning(serial); - - // Make Actions - QMenu menu; - - QAction* boot = new QAction(gameinfo->has_custom_config - ? (is_current_running_game - ? tr("&Reboot with Global Configuration") - : tr("&Boot with Global Configuration")) - : (is_current_running_game - ? tr("&Reboot") - : tr("&Boot"))); - - QFont font = boot->font(); - font.setBold(true); - - if (gameinfo->has_custom_config) - { - QAction* boot_custom = menu.addAction(is_current_running_game - ? tr("&Reboot with Custom Configuration") - : tr("&Boot with Custom Configuration")); - boot_custom->setFont(font); - connect(boot_custom, &QAction::triggered, [this, gameinfo] - { - sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT RequestBoot(gameinfo); - }); - } - else - { - boot->setFont(font); - } - - menu.addAction(boot); - - { - QAction* boot_default = menu.addAction(is_current_running_game - ? tr("&Reboot with Default Configuration") - : tr("&Boot with Default Configuration")); - - connect(boot_default, &QAction::triggered, [this, gameinfo] - { - sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT RequestBoot(gameinfo, cfg_mode::default_config); - }); - - QAction* boot_manual = menu.addAction(is_current_running_game - ? tr("&Reboot with Manually Selected Configuration") - : tr("&Boot with Manually Selected Configuration")); - - connect(boot_manual, &QAction::triggered, [this, gameinfo] - { - if (const std::string file_path = QFileDialog::getOpenFileName(this, "Select Config File", "", tr("Config Files (*.yml);;All files (*.*)")).toStdString(); !file_path.empty()) - { - sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT RequestBoot(gameinfo, cfg_mode::custom_selection, file_path); - } - else - { - sys_log.notice("Manual config selection aborted."); - } - }); - } - - extern bool is_savestate_compatible(const std::string& filepath); - - if (const std::string sstate = get_savestate_file(serial, current_game.path, 1); is_savestate_compatible(sstate)) - { - const bool has_ambiguity = !get_savestate_file(serial, current_game.path, 2).empty(); - - QAction* boot_state = menu.addAction(is_current_running_game - ? tr("&Reboot with last SaveState") - : tr("&Boot with last SaveState")); - - connect(boot_state, &QAction::triggered, [this, gameinfo, sstate] - { - sys_log.notice("Booting savestate from gamelist per context menu..."); - Q_EMIT RequestBoot(gameinfo, cfg_mode::custom, "", sstate); - }); - - if (has_ambiguity) - { - QAction* choose_state = menu.addAction(is_current_running_game - ? tr("&Choose SaveState to reboot") - : tr("&Choose SaveState to boot")); - - connect(choose_state, &QAction::triggered, [this, gameinfo] - { - // If there is any ambiguity, launch the savestate manager - Q_EMIT RequestSaveStateManager(gameinfo); - }); - } - } - - menu.addSeparator(); - - QAction* configure = menu.addAction(gameinfo->has_custom_config - ? tr("&Change Custom Configuration") - : tr("&Create Custom Configuration From Global Settings")); - QAction* create_game_default_config = gameinfo->has_custom_config ? nullptr - : menu.addAction(tr("&Create Custom Configuration From Default Settings")); - QAction* pad_configure = menu.addAction(gameinfo->has_custom_pad_config - ? tr("&Change Custom Gamepad Configuration") - : tr("&Create Custom Gamepad Configuration")); - QAction* configure_patches = menu.addAction(tr("&Manage Game Patches")); - - menu.addSeparator(); - - // Create LLVM cache - QAction* create_cpu_cache = menu.addAction(tr("&Create LLVM Cache")); - - // Remove menu - QMenu* remove_menu = menu.addMenu(tr("&Remove")); - - if (gameinfo->has_custom_config) - { - QAction* remove_custom_config = remove_menu->addAction(tr("&Remove Custom Configuration")); - connect(remove_custom_config, &QAction::triggered, [this, serial, gameinfo]() - { - if (RemoveCustomConfiguration(serial, gameinfo, true)) - { - ShowCustomConfigIcon(gameinfo); - } - }); - } - if (gameinfo->has_custom_pad_config) - { - QAction* remove_custom_pad_config = remove_menu->addAction(tr("&Remove Custom Gamepad Configuration")); - connect(remove_custom_pad_config, &QAction::triggered, [this, serial, gameinfo]() - { - if (RemoveCustomPadConfiguration(serial, gameinfo, true)) - { - ShowCustomConfigIcon(gameinfo); - } - }); - } - - const std::string cache_base_dir = fs::get_path_if_dir(rpcs3::utils::get_cache_dir_by_serial(serial)); - const bool has_hdd1_cache_dir = !rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd1_cache_dir(), serial).empty(); - const std::string savestates_dir = fs::get_path_if_dir(rpcs3::utils::get_savestates_dir(serial)); - - if (!cache_base_dir.empty()) - { - remove_menu->addSeparator(); - - QAction* remove_shader_cache = remove_menu->addAction(tr("&Remove Shader Cache")); - remove_shader_cache->setEnabled(!is_current_running_game); - connect(remove_shader_cache, &QAction::triggered, [this, serial]() - { - RemoveShaderCache(serial, true); - }); - - QAction* remove_ppu_cache = remove_menu->addAction(tr("&Remove PPU Cache")); - remove_ppu_cache->setEnabled(!is_current_running_game); - connect(remove_ppu_cache, &QAction::triggered, [this, serial]() - { - RemovePPUCache(serial, true); - }); - - QAction* remove_spu_cache = remove_menu->addAction(tr("&Remove SPU Cache")); - remove_spu_cache->setEnabled(!is_current_running_game); - connect(remove_spu_cache, &QAction::triggered, [this, serial]() - { - RemoveSPUCache(serial, true); - }); - } - - if (has_hdd1_cache_dir) - { - QAction* remove_hdd1_cache = remove_menu->addAction(tr("&Remove HDD1 Cache")); - remove_hdd1_cache->setEnabled(!is_current_running_game); - connect(remove_hdd1_cache, &QAction::triggered, [this, serial]() - { - RemoveHDD1Cache(serial, true); - }); - } - - if (!cache_base_dir.empty() || has_hdd1_cache_dir) - { - QAction* remove_all_caches = remove_menu->addAction(tr("&Remove All Caches")); - remove_all_caches->setEnabled(!is_current_running_game); - connect(remove_all_caches, &QAction::triggered, [this, serial]() - { - RemoveAllCaches(serial, true); - }); - } - - if (!savestates_dir.empty()) - { - remove_menu->addSeparator(); - - QAction* remove_savestates = remove_menu->addAction(tr("&Remove Savestates")); - remove_savestates->setEnabled(!is_current_running_game); - connect(remove_savestates, &QAction::triggered, [this, serial]() - { - SetContentList(SAVESTATES, {}); - RemoveContentList(serial, true); - }); - } - - // Disable the Remove menu if empty - remove_menu->setEnabled(!remove_menu->isEmpty()); - - menu.addSeparator(); - - // Manage Game menu - QMenu* manage_game_menu = menu.addMenu(tr("&Manage Game")); - - // Create game shortcuts - QAction* create_desktop_shortcut = manage_game_menu->addAction(tr("&Create Desktop Shortcut")); - connect(create_desktop_shortcut, &QAction::triggered, this, [this, gameinfo]() - { - CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::desktop}); - }); -#ifdef _WIN32 - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Start Menu Shortcut")); -#elif defined(__APPLE__) - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Launchpad Shortcut")); -#else - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Application Menu Shortcut")); -#endif - connect(create_start_menu_shortcut, &QAction::triggered, this, [this, gameinfo]() - { - CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::applications}); - }); - - manage_game_menu->addSeparator(); - - // Hide/rename game in game list - QAction* hide_serial = manage_game_menu->addAction(tr("&Hide In Game List")); - hide_serial->setCheckable(true); - hide_serial->setChecked(m_hidden_list.contains(QString::fromStdString(serial))); - QAction* rename_title = manage_game_menu->addAction(tr("&Rename In Game List")); - - // Edit tooltip notes/reset time played - QAction* edit_notes = manage_game_menu->addAction(tr("&Edit Tooltip Notes")); - QAction* reset_time_played = manage_game_menu->addAction(tr("&Reset Time Played")); - - manage_game_menu->addSeparator(); - - // Remove game - QAction* remove_game = manage_game_menu->addAction(tr("&Remove %1").arg(gameinfo->localized_category)); - remove_game->setEnabled(!is_current_running_game); - - // Game info - QAction* game_info = manage_game_menu->addAction(tr("&Game Info")); - connect(game_info, &QAction::triggered, this, [this, gameinfo]() - { - ShowGameInfoDialog({gameinfo}); - }); - - // Custom Images menu - QMenu* icon_menu = menu.addMenu(tr("&Custom Images")); - const std::array custom_icon_actions = - { - icon_menu->addAction(tr("&Import Custom Icon")), - icon_menu->addAction(tr("&Replace Custom Icon")), - icon_menu->addAction(tr("&Remove Custom Icon")) - }; - icon_menu->addSeparator(); - const std::array custom_gif_actions = - { - icon_menu->addAction(tr("&Import Hover Gif")), - icon_menu->addAction(tr("&Replace Hover Gif")), - icon_menu->addAction(tr("&Remove Hover Gif")) - }; - icon_menu->addSeparator(); - const std::array custom_shader_icon_actions = - { - icon_menu->addAction(tr("&Import Custom Shader Loading Background")), - icon_menu->addAction(tr("&Replace Custom Shader Loading Background")), - icon_menu->addAction(tr("&Remove Custom Shader Loading Background")) - }; - - if (const std::string custom_icon_dir_path = rpcs3::utils::get_icons_dir(serial); - fs::create_path(custom_icon_dir_path)) - { - enum class icon_action - { - add, - replace, - remove - }; - enum class icon_type - { - game_list, - hover_gif, - shader_load - }; - - const auto handle_icon = [this, serial](const QString& game_icon_path, const QString& suffix, icon_action action, icon_type type) - { - QString icon_path; - - if (action != icon_action::remove) - { - QString msg; - switch (type) - { - case icon_type::game_list: - msg = tr("Select Custom Icon"); - break; - case icon_type::hover_gif: - msg = tr("Select Custom Hover Gif"); - break; - case icon_type::shader_load: - msg = tr("Select Custom Shader Loading Background"); - break; - } - icon_path = QFileDialog::getOpenFileName(this, msg, "", tr("%0 (*.%0);;All files (*.*)").arg(suffix)); - } - if (action == icon_action::remove || !icon_path.isEmpty()) - { - bool refresh = false; - - QString msg; - switch (type) - { - case icon_type::game_list: - msg = tr("Remove Custom Icon of %0?").arg(QString::fromStdString(serial)); - break; - case icon_type::hover_gif: - msg = tr("Remove Custom Hover Gif of %0?").arg(QString::fromStdString(serial)); - break; - case icon_type::shader_load: - msg = tr("Remove Custom Shader Loading Background of %0?").arg(QString::fromStdString(serial)); - break; - } - - if (action == icon_action::replace || (action == icon_action::remove && - QMessageBox::question(this, tr("Confirm Removal"), msg) == QMessageBox::Yes)) - { - if (QFile file(game_icon_path); file.exists() && !file.remove()) - { - game_list_log.error("Could not remove old file: '%s'", game_icon_path, file.errorString()); - QMessageBox::warning(this, tr("Warning!"), tr("Failed to remove the old file!")); - return; - } - - game_list_log.success("Removed file: '%s'", game_icon_path); - if (action == icon_action::remove) - { - refresh = true; - } - } - - if (action != icon_action::remove) - { - if (!QFile::copy(icon_path, game_icon_path)) - { - game_list_log.error("Could not import file '%s' to '%s'.", icon_path, game_icon_path); - QMessageBox::warning(this, tr("Warning!"), tr("Failed to import the new file!")); - } - else - { - game_list_log.success("Imported file '%s' to '%s'", icon_path, game_icon_path); - refresh = true; - } - } - - if (refresh) - { - Refresh(true); - } - } - }; - - const std::vector&>> icon_map = - { - {icon_type::game_list, "/ICON0.PNG", "png", custom_icon_actions}, - {icon_type::hover_gif, "/hover.gif", "gif", custom_gif_actions}, - {icon_type::shader_load, "/PIC1.PNG", "png", custom_shader_icon_actions}, - }; - - for (const auto& [type, icon_name, suffix, actions] : icon_map) - { - const QString icon_path = QString::fromStdString(custom_icon_dir_path) + icon_name; - - if (QFile::exists(icon_path)) - { - actions[static_cast(icon_action::add)]->setVisible(false); - connect(actions[static_cast(icon_action::replace)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::replace, t); }); - connect(actions[static_cast(icon_action::remove)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::remove, t); }); - } - else - { - connect(actions[static_cast(icon_action::add)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::add, t); }); - actions[static_cast(icon_action::replace)]->setVisible(false); - actions[static_cast(icon_action::remove)]->setEnabled(false); - } - } - } - else - { - game_list_log.error("Could not create path '%s'", custom_icon_dir_path); - icon_menu->setEnabled(false); - } - - menu.addSeparator(); - - // Open Folder menu - QMenu* open_folder_menu = menu.addMenu(tr("&Open Folder")); - - const bool is_disc_game = QString::fromStdString(current_game.category) == cat::cat_disc_game; - const std::string data_dir = fs::get_path_if_dir(rpcs3::utils::get_data_dir(serial)); - const std::string captures_dir = fs::get_path_if_dir(rpcs3::utils::get_captures_dir()); - const std::string recordings_dir = fs::get_path_if_dir(rpcs3::utils::get_recordings_dir(serial)); - const std::string screenshots_dir = fs::get_path_if_dir(rpcs3::utils::get_screenshots_dir(serial)); - std::set data_dir_list; - - if (is_disc_game) - { - QAction* open_disc_game_folder = open_folder_menu->addAction(tr("&Open Disc Game Folder")); - connect(open_disc_game_folder, &QAction::triggered, [current_game]() - { - gui::utils::open_dir(current_game.path); - }); - - // It could be an empty list for a disc game - data_dir_list = rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd0_game_dir(), serial); - } - else - { - data_dir_list.insert(current_game.path); - } - - if (!data_dir_list.empty()) // "true" if a path is present (it could be an empty list for a disc game) - { - QAction* open_data_folder = open_folder_menu->addAction(tr("&Open %0 Folder").arg(is_disc_game ? tr("Game Data") : gameinfo->localized_category)); - connect(open_data_folder, &QAction::triggered, [data_dir_list]() - { - for (const std::string& data_dir : data_dir_list) - { - gui::utils::open_dir(data_dir); - } - }); - } - - if (gameinfo->has_custom_config) - { - QAction* open_config_folder = open_folder_menu->addAction(tr("&Open Custom Config Folder")); - connect(open_config_folder, &QAction::triggered, [serial]() - { - const std::string config_path = rpcs3::utils::get_custom_config_path(serial); - - if (fs::is_file(config_path)) - gui::utils::open_dir(config_path); - }); - } - - // This is a debug feature, let's hide it by reusing debug tab protection - if (m_gui_settings->GetValue(gui::m_showDebugTab).toBool() && !cache_base_dir.empty()) - { - QAction* open_cache_folder = open_folder_menu->addAction(tr("&Open Cache Folder")); - connect(open_cache_folder, &QAction::triggered, [cache_base_dir]() - { - gui::utils::open_dir(cache_base_dir); - }); - } - - if (!data_dir.empty()) - { - QAction* open_data_folder = open_folder_menu->addAction(tr("&Open Data Folder")); - connect(open_data_folder, &QAction::triggered, [data_dir]() - { - gui::utils::open_dir(data_dir); - }); - } - - if (!savestates_dir.empty()) - { - QAction* open_savestates_folder = open_folder_menu->addAction(tr("&Open Savestates Folder")); - connect(open_savestates_folder, &QAction::triggered, [savestates_dir]() - { - gui::utils::open_dir(savestates_dir); - }); - } - - if (!captures_dir.empty()) - { - QAction* open_captures_folder = open_folder_menu->addAction(tr("&Open Captures Folder")); - connect(open_captures_folder, &QAction::triggered, [captures_dir]() - { - gui::utils::open_dir(captures_dir); - }); - } - - if (!recordings_dir.empty()) - { - QAction* open_recordings_folder = open_folder_menu->addAction(tr("&Open Recordings Folder")); - connect(open_recordings_folder, &QAction::triggered, [recordings_dir]() - { - gui::utils::open_dir(recordings_dir); - }); - } - - if (!screenshots_dir.empty()) - { - QAction* open_screenshots_folder = open_folder_menu->addAction(tr("&Open Screenshots Folder")); - connect(open_screenshots_folder, &QAction::triggered, [screenshots_dir]() - { - gui::utils::open_dir(screenshots_dir); - }); - } - - // Copy Info menu - QMenu* info_menu = menu.addMenu(tr("&Copy Info")); - QAction* copy_info = info_menu->addAction(tr("&Copy Name + Serial")); - QAction* copy_name = info_menu->addAction(tr("&Copy Name")); - QAction* copy_serial = info_menu->addAction(tr("&Copy Serial")); - - menu.addSeparator(); - - QAction* check_compat = menu.addAction(tr("&Check Game Compatibility")); - QAction* download_compat = menu.addAction(tr("&Download Compatibility Database")); - - connect(boot, &QAction::triggered, this, [this, gameinfo]() - { - sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT RequestBoot(gameinfo, cfg_mode::global); - }); - - auto configure_l = [this, current_game, gameinfo](bool create_cfg_from_global_cfg) - { - settings_dialog dlg(m_gui_settings, m_emu_settings, 0, this, ¤t_game, create_cfg_from_global_cfg); - - connect(&dlg, &settings_dialog::EmuSettingsApplied, [this, gameinfo]() - { - if (!gameinfo->has_custom_config) - { - gameinfo->has_custom_config = true; - ShowCustomConfigIcon(gameinfo); - } - Q_EMIT NotifyEmuSettingsChange(); - }); - - dlg.exec(); - }; - - if (create_game_default_config) - { - connect(configure, &QAction::triggered, this, [configure_l]() { configure_l(true); }); - connect(create_game_default_config, &QAction::triggered, this, [configure_l = std::move(configure_l)]() { configure_l(false); }); - } - else - { - connect(configure, &QAction::triggered, this, [configure_l = std::move(configure_l)]() { configure_l(true); }); - } - - connect(pad_configure, &QAction::triggered, this, [this, current_game, gameinfo]() - { - pad_settings_dialog dlg(m_gui_settings, this, ¤t_game); - - if (dlg.exec() == QDialog::Accepted && !gameinfo->has_custom_pad_config) - { - gameinfo->has_custom_pad_config = true; - ShowCustomConfigIcon(gameinfo); - } - }); - connect(hide_serial, &QAction::triggered, this, [serial = QString::fromStdString(serial), this](bool checked) - { - if (checked) - m_hidden_list.insert(serial); - else - m_hidden_list.remove(serial); - - m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_hidden_list.values())); - Refresh(); - }); - connect(create_cpu_cache, &QAction::triggered, this, [gameinfo, this] - { - if (m_gui_settings->GetBootConfirmation(this)) - { - CreateCPUCaches(gameinfo); - } - }); - connect(remove_game, &QAction::triggered, this, [this, gameinfo] - { - ShowRemoveGameDialog({gameinfo}); - }); - connect(configure_patches, &QAction::triggered, this, [this, gameinfo]() - { - patch_manager_dialog patch_manager(m_gui_settings, m_game_data, gameinfo->info.serial, gameinfo->GetGameVersion(), this); - patch_manager.exec(); - }); - connect(check_compat, &QAction::triggered, this, [serial = QString::fromStdString(serial)] - { - const QString link = "https://rpcs3.net/compatibility?g=" + serial; - QDesktopServices::openUrl(QUrl(link)); - }); - connect(download_compat, &QAction::triggered, this, [this] - { - m_game_compat->RequestCompatibility(true); - }); - connect(rename_title, &QAction::triggered, this, [this, name, serial = QString::fromStdString(serial), global_pos] - { - const QString custom_title = m_persistent_settings->GetValue(gui::persistent::titles, serial, "").toString(); - const QString old_title = custom_title.isEmpty() ? name : custom_title; - - input_dialog dlg(128, old_title, tr("Rename Title"), tr("%0\n%1\n\nYou can clear the line in order to use the original title.").arg(name).arg(serial), name, this); - dlg.move(global_pos); - - if (dlg.exec() == QDialog::Accepted) - { - const QString new_title = dlg.get_input_text().simplified(); - - if (new_title.isEmpty() || new_title == name) - { - m_titles.erase(serial); - m_persistent_settings->RemoveValue(gui::persistent::titles, serial); - } - else - { - m_titles.insert_or_assign(serial, new_title); - m_persistent_settings->SetValue(gui::persistent::titles, serial, new_title); - } - Refresh(true); // full refresh in order to reliably sort the list - } - }); - connect(edit_notes, &QAction::triggered, this, [this, name, serial = QString::fromStdString(serial)] - { - bool accepted; - const QString old_notes = m_persistent_settings->GetValue(gui::persistent::notes, serial, "").toString(); - const QString new_notes = QInputDialog::getMultiLineText(this, tr("Edit Tooltip Notes"), tr("%0\n%1").arg(name).arg(serial), old_notes, &accepted); - - if (accepted) - { - if (new_notes.simplified().isEmpty()) - { - m_notes.erase(serial); - m_persistent_settings->RemoveValue(gui::persistent::notes, serial); - } - else - { - m_notes.insert_or_assign(serial, new_notes); - m_persistent_settings->SetValue(gui::persistent::notes, serial, new_notes); - } - Refresh(); - } - }); - connect(reset_time_played, &QAction::triggered, this, [this, name, serial = QString::fromStdString(serial)] - { - if (QMessageBox::question(this, tr("Confirm Reset"), tr("Reset time played?\n\n%0 [%1]").arg(name).arg(serial)) == QMessageBox::Yes) - { - m_persistent_settings->SetPlaytime(serial, 0, false); - m_persistent_settings->SetLastPlayed(serial, 0, true); - Refresh(); - } - }); - connect(copy_info, &QAction::triggered, this, [name, serial = QString::fromStdString(serial)] - { - QApplication::clipboard()->setText(name % QStringLiteral(" [") % serial % QStringLiteral("]")); - }); - connect(copy_name, &QAction::triggered, this, [name] - { - QApplication::clipboard()->setText(name); - }); - connect(copy_serial, &QAction::triggered, this, [serial = QString::fromStdString(serial)] - { - QApplication::clipboard()->setText(serial); - }); - - // Disable options depending on software category - const QString category = QString::fromStdString(current_game.category); - - if (category == cat::cat_ps3_os) - { - remove_game->setEnabled(false); - } - else if (category != cat::cat_disc_game && category != cat::cat_hdd_game) - { - check_compat->setEnabled(false); - } - - menu.exec(global_pos); -} - -void game_list_frame::ShowMultiSelectionContextMenu(const std::vector& games, QPoint& global_pos) -{ - // Make Actions - QMenu menu; - - // Create LLVM cache - QAction* create_cpu_cache = menu.addAction(tr("&Create LLVM Cache")); - connect(create_cpu_cache, &QAction::triggered, [this, games]() - { - BatchCreateCPUCaches(games, false, true); - }); - - // Remove menu - QMenu* remove_menu = menu.addMenu(tr("&Remove")); - - QAction* remove_custom_config = remove_menu->addAction(tr("&Remove Custom Configuration")); - connect(remove_custom_config, &QAction::triggered, [this, games]() - { - BatchRemoveCustomConfigurations(games, true); - }); - - QAction* remove_custom_pad_config = remove_menu->addAction(tr("&Remove Custom Gamepad Configuration")); - connect(remove_custom_pad_config, &QAction::triggered, [this, games]() - { - BatchRemoveCustomPadConfigurations(games, true); - }); - - remove_menu->addSeparator(); - - QAction* remove_shader_cache = remove_menu->addAction(tr("&Remove Shader Cache")); - connect(remove_shader_cache, &QAction::triggered, [this, games]() - { - BatchRemoveShaderCaches(games, true); - }); - - QAction* remove_ppu_cache = remove_menu->addAction(tr("&Remove PPU Cache")); - connect(remove_ppu_cache, &QAction::triggered, [this, games]() - { - BatchRemovePPUCaches(games, true); - }); - - QAction* remove_spu_cache = remove_menu->addAction(tr("&Remove SPU Cache")); - connect(remove_spu_cache, &QAction::triggered, [this, games]() - { - BatchRemoveSPUCaches(games, true); - }); - - QAction* remove_hdd1_cache = remove_menu->addAction(tr("&Remove HDD1 Cache")); - connect(remove_hdd1_cache, &QAction::triggered, [this, games]() - { - BatchRemoveHDD1Caches(games, true); - }); - - QAction* remove_all_caches = remove_menu->addAction(tr("&Remove All Caches")); - connect(remove_all_caches, &QAction::triggered, [this, games]() - { - BatchRemoveAllCaches(games, true); - }); - - remove_menu->addSeparator(); - - QAction* remove_savestates = remove_menu->addAction(tr("&Remove Savestates")); - connect(remove_savestates, &QAction::triggered, [this, games]() - { - SetContentList(SAVESTATES, {}); - BatchRemoveContentLists(games, true); - }); - - // Disable the Remove menu if empty - remove_menu->setEnabled(!remove_menu->isEmpty()); - - menu.addSeparator(); - - // Manage Game menu - QMenu* manage_game_menu = menu.addMenu(tr("&Manage Game")); - - // Create game shortcuts - QAction* create_desktop_shortcut = manage_game_menu->addAction(tr("&Create Desktop Shortcut")); - connect(create_desktop_shortcut, &QAction::triggered, this, [this, games]() - { - if (QMessageBox::question(this, tr("Confirm Creation"), tr("Create desktop shortcut?")) != QMessageBox::Yes) - return; - - CreateShortcuts(games, {gui::utils::shortcut_location::desktop}); - }); - -#ifdef _WIN32 - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Start Menu Shortcut")); -#elif defined(__APPLE__) - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Launchpad Shortcut")); -#else - QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Application Menu Shortcut")); -#endif - connect(create_start_menu_shortcut, &QAction::triggered, this, [this, games]() - { - if (QMessageBox::question(this, tr("Confirm Creation"), tr("Create shortcut?")) != QMessageBox::Yes) - return; - - CreateShortcuts(games, {gui::utils::shortcut_location::applications}); - }); - - manage_game_menu->addSeparator(); - - // Hide game in game list - QAction* hide_serial = manage_game_menu->addAction(tr("&Hide In Game List")); - connect(hide_serial, &QAction::triggered, this, [this, games]() - { - if (QMessageBox::question(this, tr("Confirm Hiding"), tr("Hide in game list?")) != QMessageBox::Yes) - return; - - for (const auto& game : games) - { - m_hidden_list.insert(QString::fromStdString(game->info.serial)); - } - - m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_hidden_list.values())); - Refresh(); - }); - - // Show game in game list - QAction* show_serial = manage_game_menu->addAction(tr("&Show In Game List")); - connect(show_serial, &QAction::triggered, this, [this, games]() - { - for (const auto& game : games) - { - m_hidden_list.remove(QString::fromStdString(game->info.serial)); - } - - m_gui_settings->SetValue(gui::gl_hidden_list, QStringList(m_hidden_list.values())); - Refresh(); - }); - - manage_game_menu->addSeparator(); - - // Reset time played - QAction* reset_time_played = manage_game_menu->addAction(tr("&Reset Time Played")); - connect(reset_time_played, &QAction::triggered, this, [this, games]() - { - if (QMessageBox::question(this, tr("Confirm Reset"), tr("Reset time played?")) != QMessageBox::Yes) - return; - - for (const auto& game : games) - { - const auto serial = QString::fromStdString(game->info.serial); - - m_persistent_settings->SetPlaytime(serial, 0, false); - m_persistent_settings->SetLastPlayed(serial, 0, true); - } - - Refresh(); - }); - - manage_game_menu->addSeparator(); - - // Remove game - QAction* remove_game = manage_game_menu->addAction(tr("&Remove Game")); - connect(remove_game, &QAction::triggered, this, [this, games]() - { - ShowRemoveGameDialog(games); - }); - - // Game info - QAction* game_info = manage_game_menu->addAction(tr("&Game Info")); - connect(game_info, &QAction::triggered, this, [this, games]() - { - ShowGameInfoDialog(games); - }); - - menu.exec(global_pos); -} - void game_list_frame::ShowContextMenu(const QPoint& pos) { QPoint global_pos; @@ -2026,10 +969,10 @@ void game_list_frame::ShowContextMenu(const QPoint& pos) { global_pos = m_game_list->viewport()->mapToGlobal(pos); - auto item_list = m_game_list->selectedItems(); + const auto item_list = m_game_list->selectedItems(); game_info gameinfo; - for (auto item : item_list) + for (const auto& item : item_list) { if (item->column() != static_cast(gui::game_list_columns::icon)) continue; @@ -2046,1392 +989,13 @@ void game_list_frame::ShowContextMenu(const QPoint& pos) games.push_back(gameinfo); } - switch (games.size()) + if (!games.empty()) { - case 0: - return; - case 1: - ShowSingleSelectionContextMenu(games[0], global_pos); - break; - default: - ShowMultiSelectionContextMenu(games, global_pos); - break; + game_list_context_menu menu(this); + menu.show_menu(games, global_pos); } } -void game_list_frame::SetContentList(u16 content_types, const content_info& content_info) -{ - m_content_info = content_info; - - m_content_info.content_types = content_types; - m_content_info.clear_on_finish = true; // Always overridden by BatchRemoveContentLists() -} - -void game_list_frame::ClearContentList(bool refresh) -{ - if (refresh) - { - std::vector serials_to_remove_from_yml; - - // Prepare the list of serials (title id) to remove in "games.yml" file (if any) - for (const auto& removedDisc : m_content_info.removed_disc_list) - { - serials_to_remove_from_yml.push_back(removedDisc); - } - - // Finally, refresh the game list - Refresh(true, serials_to_remove_from_yml); - } - - m_content_info = {NO_CONTENT}; -} - -game_list_frame::content_info game_list_frame::GetContentInfo(const std::vector& games) -{ - content_info content_info = {NO_CONTENT}; - - if (games.empty()) - return content_info; - - bool is_disc_game = false; - u64 total_disc_size = 0; - u64 total_data_size = 0; - QString text; - - // Fill in content_info - - content_info.is_single_selection = games.size() == 1; - - for (const auto& game : games) - { - GameInfo& current_game = game->info; - - is_disc_game = QString::fromStdString(current_game.category) == cat::cat_disc_game; - - // +1 if it's a disc game's path and it's present in the shared games folder - content_info.in_games_dir_count += (is_disc_game && Emu.IsPathInsideDir(current_game.path, rpcs3::utils::get_games_dir())) ? 1 : 0; - - // Add the name to the content's name list for the related serial - content_info.name_list[current_game.serial].insert(current_game.name); - - if (is_disc_game) - { - if (current_game.size_on_disk != umax) // If size was properly detected - total_disc_size += current_game.size_on_disk; - - // Add the serial to the disc list - content_info.disc_list.insert(current_game.serial); - - // It could be an empty list for a disc game - std::set data_dir_list = rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd0_game_dir(), current_game.serial); - - // Add the path list to the content's path list for the related serial - for (const auto& data_dir : data_dir_list) - { - content_info.path_list[current_game.serial].insert(data_dir); - } - } - else - { - // Add the path to the content's path list for the related serial - content_info.path_list[current_game.serial].insert(current_game.path); - } - } - - // Fill in text based on filled in content_info - - if (content_info.is_single_selection) // Single selection - { - GameInfo& current_game = games[0]->info; - - text = tr("%0 - %1\n").arg(QString::fromStdString(current_game.serial)).arg(QString::fromStdString(current_game.name)); - - if (is_disc_game) - { - text += tr("\nDisc Game Info:\nPath: %0\n").arg(QString::fromStdString(current_game.path)); - - if (total_disc_size) - text += tr("Size: %0\n").arg(gui::utils::format_byte_size(total_disc_size)); - } - - // if a path is present (it could be an empty list for a disc game) - if (const auto& it = content_info.path_list.find(current_game.serial); it != content_info.path_list.end()) - { - text += tr("\n%0 Info:\n").arg(is_disc_game ? tr("Game Data") : games[0]->localized_category); - - for (const auto& data_dir : it->second) - { - text += tr("Path: %0\n").arg(QString::fromStdString(data_dir)); - - if (const u64 data_size = fs::get_dir_size(data_dir, 1); data_size != umax) - { // If size was properly detected - total_data_size += data_size; - text += tr("Size: %0\n").arg(gui::utils::format_byte_size(data_size)); - } - } - - if (it->second.size() > 1) - text += tr("Total size: %0\n").arg(gui::utils::format_byte_size(total_data_size)); - } - } - else // Multi selection - { - for (const auto& [serial, data_dir_list] : content_info.path_list) - { - for (const auto& data_dir : data_dir_list) - { - if (const u64 data_size = fs::get_dir_size(data_dir, 1); data_size != umax) // If size was properly detected - total_data_size += data_size; - } - } - - text = tr("%0 selected games: %1 Disc Game - %2 not Disc Game\n").arg(games.size()) - .arg(content_info.disc_list.size()).arg(games.size() - content_info.disc_list.size()); - - text += tr("\nDisc Game Info:\n"); - - if (content_info.disc_list.size() != content_info.in_games_dir_count) - text += tr("VFS unhosted: %0\n").arg(content_info.disc_list.size() - content_info.in_games_dir_count); - - if (content_info.in_games_dir_count) - text += tr("VFS hosted: %0\n").arg(content_info.in_games_dir_count); - - if (content_info.disc_list.size() != content_info.in_games_dir_count && content_info.in_games_dir_count) - text += tr("Total games: %0\n").arg((content_info.disc_list.size() - content_info.in_games_dir_count) + content_info.in_games_dir_count); - - if (total_disc_size) - text += tr("Total size: %0\n").arg(gui::utils::format_byte_size(total_disc_size)); - - if (content_info.path_list.size()) - text += tr("\nGame Data Info:\nTotal size: %0\n").arg(gui::utils::format_byte_size(total_data_size)); - } - - u64 caches_size = 0; - u64 icons_size = 0; - u64 savestates_size = 0; - u64 captures_size = 0; - u64 recordings_size = 0; - u64 screenshots_size = 0; - - for (const auto& [serial, name_list] : content_info.name_list) - { - // Main cache - if (const u64 size = fs::get_dir_size(rpcs3::utils::get_cache_dir_by_serial(serial), 1); size != umax) - caches_size += size; - - // HDD1 cache - for (const auto& dir : rpcs3::utils::get_dir_list(rpcs3::utils::get_hdd1_cache_dir(), serial)) - { - if (const u64 size = fs::get_dir_size(dir, 1); size != umax) - caches_size += size; - } - - if (const u64 size = fs::get_dir_size(rpcs3::utils::get_icons_dir(serial), 1); size != umax) - icons_size += size; - - if (const u64 size = fs::get_dir_size(rpcs3::utils::get_savestates_dir(serial), 1); size != umax) - savestates_size += size; - - for (const auto& file : rpcs3::utils::get_file_list(rpcs3::utils::get_captures_dir(), serial)) - { - if (fs::stat_t stat{}; fs::get_stat(file, stat)) - captures_size += stat.size; - } - - if (const u64 size = fs::get_dir_size(rpcs3::utils::get_recordings_dir(serial), 1); size != umax) - recordings_size += size; - - if (const u64 size = fs::get_dir_size(rpcs3::utils::get_screenshots_dir(serial), 1); size != umax) - screenshots_size += size; - } - - text += tr("\nEmulator Data Info:\nCaches size: %0\n").arg(gui::utils::format_byte_size(caches_size)); - text += tr("Icons size: %0\n").arg(gui::utils::format_byte_size(icons_size)); - text += tr("Savestates size: %0\n").arg(gui::utils::format_byte_size(savestates_size)); - text += tr("Captures size: %0\n").arg(gui::utils::format_byte_size(captures_size)); - text += tr("Recordings size: %0\n").arg(gui::utils::format_byte_size(recordings_size)); - text += tr("Screenshots size: %0\n").arg(gui::utils::format_byte_size(screenshots_size)); - - // Retrieve disk space info on data path's drive - if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat)) - text += tr("\nCurrent free disk space: %0\n").arg(gui::utils::format_byte_size(stat.avail_free)); - - content_info.info = text; - - return content_info; -} - -void game_list_frame::ShowRemoveGameDialog(const std::vector& games) -{ - if (games.empty()) - return; - - content_info content_info = GetContentInfo(games); - QString text = content_info.info; - - QCheckBox* disc = new QCheckBox(tr("Remove title from game list (Disc Game path is not removed!)")); - QCheckBox* caches = new QCheckBox(tr("Remove caches and custom configs")); - QCheckBox* icons = new QCheckBox(tr("Remove icons and shortcuts")); - QCheckBox* savestate = new QCheckBox(tr("Remove savestates")); - QCheckBox* captures = new QCheckBox(tr("Remove captures")); - QCheckBox* recordings = new QCheckBox(tr("Remove recordings")); - QCheckBox* screenshots = new QCheckBox(tr("Remove screenshots")); - - if (content_info.disc_list.size()) - { - if (content_info.in_games_dir_count == content_info.disc_list.size()) - { - disc->setToolTip(tr("Title located under auto-detection VFS \"games\" folder cannot be removed")); - disc->setDisabled(true); - } - else - { - if (!content_info.is_single_selection) // Multi selection - disc->setToolTip(tr("Title located under auto-detection VFS \"games\" folder cannot be removed")); - - disc->setChecked(true); - } - } - else - { - disc->setChecked(false); - disc->setVisible(false); - } - - if (content_info.path_list.size()) // If a path is present - { - text += tr("\nPermanently remove %0 and selected (optional) contents from drive?\n") - .arg((content_info.disc_list.size() || !content_info.is_single_selection) - ? tr("Game Data") : games[0]->localized_category); - } - else - { - text += tr("\nPermanently remove selected (optional) contents from drive?\n"); - } - - caches->setChecked(true); - icons->setChecked(true); - - QMessageBox mb(QMessageBox::Question, tr("Confirm Removal"), text, QMessageBox::Yes | QMessageBox::No, this); - mb.setCheckBox(disc); - - QGridLayout* grid = qobject_cast(mb.layout()); - int row, column, rowSpan, columnSpan; - - grid->getItemPosition(grid->indexOf(disc), &row, &column, &rowSpan, &columnSpan); - grid->addWidget(caches, row + 3, column, rowSpan, columnSpan); - grid->addWidget(icons, row + 4, column, rowSpan, columnSpan); - grid->addWidget(savestate, row + 5, column, rowSpan, columnSpan); - grid->addWidget(captures, row + 6, column, rowSpan, columnSpan); - grid->addWidget(recordings, row + 7, column, rowSpan, columnSpan); - grid->addWidget(screenshots, row + 8, column, rowSpan, columnSpan); - - if (mb.exec() != QMessageBox::Yes) - return; - - // Remove data path in "dev_hdd0/game" folder (if any) and lock file in "dev_hdd0/game/$locks" folder (if any) - u16 content_types = DATA | LOCKS; - - // Remove serials (title id) in "games.yml" file (if any) - if (disc->isChecked()) - content_types |= DISC; - - // Remove caches in "cache" and "dev_hdd1/caches" folders (if any) and custom configs in "config/custom_config" folder (if any) - if (caches->isChecked()) - content_types |= CACHES | CUSTOM_CONFIG; - - // Remove icons in "Icons/game_icons" folder (if any) and - // shortcuts in "games/shortcuts" folder and from desktop / start menu (if any) - if (icons->isChecked()) - content_types |= ICONS | SHORTCUTS; - - if (savestate->isChecked()) - content_types |= SAVESTATES; - - if (captures->isChecked()) - content_types |= CAPTURES; - - if (recordings->isChecked()) - content_types |= RECORDINGS; - - if (screenshots->isChecked()) - content_types |= SCREENSHOTS; - - SetContentList(content_types, content_info); - - if (content_info.is_single_selection) // Single selection - { - if (!RemoveContentList(games[0]->info.serial, true)) - { - QMessageBox::critical(this, tr("Failure!"), caches->isChecked() - ? tr("Failed to remove %0 from drive!\nCaches and custom configs have been left intact.").arg(QString::fromStdString(games[0]->info.name)) - : tr("Failed to remove %0 from drive!").arg(QString::fromStdString(games[0]->info.name))); - - return; - } - } - else // Multi selection - { - BatchRemoveContentLists(games, true); - } -} - -void game_list_frame::ShowGameInfoDialog(const std::vector& games) -{ - if (games.empty()) - return; - - QMessageBox::information(this, tr("Game Info"), GetContentInfo(games).info); -} - -bool game_list_frame::IsGameRunning(const std::string& serial) -{ - return !Emu.IsStopped(true) && (serial == Emu.GetTitleID() || (serial == "vsh.self" && Emu.IsVsh())); -} - -bool game_list_frame::ValidateRemoval(const std::string& serial, const std::string& path, const std::string& desc, bool is_interactive) -{ - if (serial.empty()) - { - game_list_log.error("Removal of %s not allowed due to no title ID provided!", desc); - return false; - } - - if (path.empty() || !fs::exists(path) || (!fs::is_dir(path) && !fs::is_file(path))) - { - game_list_log.success("Could not find %s directory/file: %s (%s)", desc, path, serial); - return false; - } - - if (is_interactive) - { - if (IsGameRunning(serial)) - { - game_list_log.error("Removal of %s not allowed due to %s title is running!", desc, serial); - - QMessageBox::critical(this, tr("Removal Aborted"), - tr("Removal of %0 not allowed due to %1 title is running!") - .arg(QString::fromStdString(desc)).arg(QString::fromStdString(serial))); - - return false; - } - - if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove %0?").arg(QString::fromStdString(desc))) != QMessageBox::Yes) - return false; - } - - return true; -} - -bool game_list_frame::ValidateBatchRemoval(const std::string& desc, bool is_interactive) -{ - if (!Emu.IsStopped(true)) - { - game_list_log.error("Removal of %s not allowed due to emulator is running!", desc); - - if (is_interactive) - { - QMessageBox::critical(this, tr("Removal Aborted"), - tr("Removal of %0 not allowed due to emulator is running!").arg(QString::fromStdString(desc))); - } - - return false; - } - - if (is_interactive) - { - if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove %0?").arg(QString::fromStdString(desc))) != QMessageBox::Yes) - return false; - } - - return true; -} - -bool game_list_frame::CreateCPUCaches(const std::string& path, const std::string& serial, bool is_fast_compilation) -{ - Emu.GracefulShutdown(false); - Emu.SetForceBoot(true); - Emu.SetPrecompileCacheOption(emu_precompilation_option_t{.is_fast = is_fast_compilation}); - - if (const auto error = Emu.BootGame(fs::is_file(path) ? fs::get_parent_dir(path) : path, serial, true); error != game_boot_result::no_errors) - { - game_list_log.error("Could not create LLVM caches for %s, error: %s", path, error); - return false; - } - - game_list_log.warning("Creating LLVM Caches for %s", path); - return true; -} - -bool game_list_frame::CreateCPUCaches(const game_info& game, bool is_fast_compilation) -{ - return game && CreateCPUCaches(game->info.path, game->info.serial, is_fast_compilation); -} - -bool game_list_frame::RemoveCustomConfiguration(const std::string& serial, const game_info& game, bool is_interactive) -{ - const std::string path = rpcs3::utils::get_custom_config_path(serial); - - if (!ValidateRemoval(serial, path, "custom configuration", is_interactive)) - return true; - - bool result = true; - - if (fs::is_file(path)) - { - if (fs::remove_file(path)) - { - if (game) - { - game->has_custom_config = false; - } - game_list_log.success("Removed configuration file: %s", path); - } - else - { - game_list_log.fatal("Failed to remove configuration file: %s\nError: %s", path, fs::g_tls_error); - result = false; - } - } - - if (is_interactive && !result) - { - QMessageBox::warning(this, tr("Warning!"), tr("Failed to remove configuration file!")); - } - - return result; -} - -bool game_list_frame::RemoveCustomPadConfiguration(const std::string& serial, const game_info& game, bool is_interactive) -{ - const std::string config_dir = rpcs3::utils::get_input_config_dir(serial); - - if (!ValidateRemoval(serial, config_dir, "custom gamepad configuration", false)) // no interation needed here - return true; - - if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), (!Emu.IsStopped(true) && Emu.GetTitleID() == serial) - ? tr("Remove custom gamepad configuration?\nYour configuration will revert to the global pad settings.") - : tr("Remove custom gamepad configuration?")) != QMessageBox::Yes) - return true; - - g_cfg_input_configs.load(); - g_cfg_input_configs.active_configs.erase(serial); - g_cfg_input_configs.save(); - game_list_log.notice("Removed active input configuration entry for key '%s'", serial); - - if (QDir(QString::fromStdString(config_dir)).removeRecursively()) - { - if (game) - { - game->has_custom_pad_config = false; - } - if (!Emu.IsStopped(true) && Emu.GetTitleID() == serial) - { - pad::set_enabled(false); - pad::reset(serial); - pad::set_enabled(true); - } - game_list_log.notice("Removed gamepad configuration directory: %s", config_dir); - return true; - } - - if (is_interactive) - { - QMessageBox::warning(this, tr("Warning!"), tr("Failed to completely remove gamepad configuration directory!")); - game_list_log.fatal("Failed to completely remove gamepad configuration directory: %s\nError: %s", config_dir, fs::g_tls_error); - } - return false; -} - -bool game_list_frame::RemoveShaderCache(const std::string& serial, bool is_interactive) -{ - const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); - - if (!ValidateRemoval(serial, base_dir, "shader cache", is_interactive)) - return true; - - u32 caches_removed = 0; - u32 caches_total = 0; - - const QStringList filter{ QStringLiteral("shaders_cache") }; - const QString q_base_dir = QString::fromStdString(base_dir); - - QDirIterator dir_iter(q_base_dir, filter, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - - if (QDir(filepath).removeRecursively()) - { - ++caches_removed; - game_list_log.notice("Removed shader cache directory: %s", filepath); - } - else - { - game_list_log.warning("Could not completely remove shader cache directory: %s", filepath); - } - - ++caches_total; - } - - const bool success = caches_total == caches_removed; - - if (success) - game_list_log.success("Removed shader cache in %s", base_dir); - else - game_list_log.fatal("Only %d/%d shader cache directories could be removed in %s", caches_removed, caches_total, base_dir); - - if (QDir(q_base_dir).isEmpty()) - { - if (fs::remove_dir(base_dir)) - game_list_log.notice("Removed empty shader cache directory: %s", base_dir); - else - game_list_log.error("Could not remove empty shader cache directory: '%s' (%s)", base_dir, fs::g_tls_error); - } - - return success; -} - -bool game_list_frame::RemovePPUCache(const std::string& serial, bool is_interactive) -{ - const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); - - if (!ValidateRemoval(serial, base_dir, "PPU cache", is_interactive)) - return true; - - u32 files_removed = 0; - u32 files_total = 0; - - const QStringList filter{ QStringLiteral("v*.obj"), QStringLiteral("v*.obj.gz") }; - const QString q_base_dir = QString::fromStdString(base_dir); - - QDirIterator dir_iter(q_base_dir, filter, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - - if (QFile::remove(filepath)) - { - ++files_removed; - game_list_log.notice("Removed PPU cache file: %s", filepath); - } - else - { - game_list_log.warning("Could not remove PPU cache file: %s", filepath); - } - - ++files_total; - } - - const bool success = files_total == files_removed; - - if (success) - game_list_log.success("Removed PPU cache in %s", base_dir); - else - game_list_log.fatal("Only %d/%d PPU cache files could be removed in %s", files_removed, files_total, base_dir); - - if (QDir(q_base_dir).isEmpty()) - { - if (fs::remove_dir(base_dir)) - game_list_log.notice("Removed empty PPU cache directory: %s", base_dir); - else - game_list_log.error("Could not remove empty PPU cache directory: '%s' (%s)", base_dir, fs::g_tls_error); - } - - return success; -} - -bool game_list_frame::RemoveSPUCache(const std::string& serial, bool is_interactive) -{ - const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); - - if (!ValidateRemoval(serial, base_dir, "SPU cache", is_interactive)) - return true; - - u32 files_removed = 0; - u32 files_total = 0; - - const QStringList filter{ QStringLiteral("spu*.dat"), QStringLiteral("spu*.dat.gz"), QStringLiteral("spu*.obj"), QStringLiteral("spu*.obj.gz") }; - const QString q_base_dir = QString::fromStdString(base_dir); - - QDirIterator dir_iter(q_base_dir, filter, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - - if (QFile::remove(filepath)) - { - ++files_removed; - game_list_log.notice("Removed SPU cache file: %s", filepath); - } - else - { - game_list_log.warning("Could not remove SPU cache file: %s", filepath); - } - - ++files_total; - } - - const bool success = files_total == files_removed; - - if (success) - game_list_log.success("Removed SPU cache in %s", base_dir); - else - game_list_log.fatal("Only %d/%d SPU cache files could be removed in %s", files_removed, files_total, base_dir); - - if (QDir(q_base_dir).isEmpty()) - { - if (fs::remove_dir(base_dir)) - game_list_log.notice("Removed empty SPU cache directory: %s", base_dir); - else - game_list_log.error("Could not remove empty SPU cache directory: '%s' (%s)", base_dir, fs::g_tls_error); - } - - return success; -} - -bool game_list_frame::RemoveHDD1Cache(const std::string& serial, bool is_interactive) -{ - const std::string base_dir = rpcs3::utils::get_hdd1_cache_dir(); - - if (!ValidateRemoval(serial, base_dir, "HDD1 cache", is_interactive)) - return true; - - u32 dirs_removed = 0; - u32 dirs_total = 0; - - const QStringList filter{ QString::fromStdString(serial + "_*") }; - const QString q_base_dir = QString::fromStdString(base_dir); - - QDirIterator dir_iter(q_base_dir, filter, QDir::Dirs | QDir::NoDotAndDotDot); - - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - - if (fs::remove_all(filepath.toStdString())) - { - ++dirs_removed; - game_list_log.notice("Removed HDD1 cache directory: %s", filepath); - } - else - { - game_list_log.warning("Could not completely remove HDD1 cache directory: %s", filepath); - } - - ++dirs_total; - } - - const bool success = dirs_removed == dirs_total; - - if (success) - game_list_log.success("Removed HDD1 cache in %s (%s)", base_dir, serial); - else - game_list_log.fatal("Only %d/%d HDD1 cache directories could be removed in %s (%s)", dirs_removed, dirs_total, base_dir, serial); - - return success; -} - -bool game_list_frame::RemoveAllCaches(const std::string& serial, bool is_interactive) -{ - // Just used for confirmation, if requested. Folder returned by fs::get_config_dir() is always present! - if (!ValidateRemoval(serial, fs::get_config_dir(), "all caches", is_interactive)) - return true; - - const std::string base_dir = rpcs3::utils::get_cache_dir_by_serial(serial); - - if (!ValidateRemoval(serial, base_dir, "main cache", false)) // no interation needed here - return true; - - bool success = false; - - if (fs::remove_all(base_dir)) - { - success = true; - game_list_log.success("Removed main cache in %s", base_dir); - - } - else - { - game_list_log.fatal("Could not completely remove main cache in %s (%s)", base_dir, serial); - } - - success |= RemoveHDD1Cache(serial); - - return success; -} - -bool game_list_frame::RemoveContentList(const std::string& serial, bool is_interactive) -{ - // Just used for confirmation, if requested. Folder returned by fs::get_config_dir() is always present! - if (!ValidateRemoval(serial, fs::get_config_dir(), "selected content", is_interactive)) - { - if (m_content_info.clear_on_finish) - ClearContentList(); // Clear only the content's info - - return true; - } - - u16 content_types = m_content_info.content_types; - - // Remove data path in "dev_hdd0/game" folder (if any) - if (content_types & DATA) - { - if (const auto it = m_content_info.path_list.find(serial); it != m_content_info.path_list.cend()) - { - if (RemoveContentPathList(it->second, "data") != it->second.size()) - { - if (m_content_info.clear_on_finish) - ClearContentList(); // Clear only the content's info - - // Skip the removal of the remaining selected contents in case some data paths could not be removed - return false; - } - } - } - - // Add serial (title id) to the list of serials to be removed in "games.yml" file (if any) - if (content_types & DISC) - { - if (const auto it = m_content_info.disc_list.find(serial); it != m_content_info.disc_list.cend()) - m_content_info.removed_disc_list.insert(serial); - } - - // Remove lock file in "dev_hdd0/game/$locks" folder (if any) - if (content_types & LOCKS) - { - if (ValidateRemoval(serial, rpcs3::utils::get_hdd0_locks_dir(), "lock")) - RemoveContentBySerial(rpcs3::utils::get_hdd0_locks_dir(), serial, "lock"); - } - - // Remove caches in "cache" and "dev_hdd1/caches" folders (if any) - if (content_types & CACHES) - { - if (ValidateRemoval(serial, rpcs3::utils::get_cache_dir_by_serial(serial), "all caches")) - RemoveAllCaches(serial); - } - - // Remove custom configs in "config/custom_config" folder (if any) - if (content_types & CUSTOM_CONFIG) - { - if (ValidateRemoval(serial, rpcs3::utils::get_custom_config_path(serial), "custom configuration")) - RemoveCustomConfiguration(serial); - - if (ValidateRemoval(serial, rpcs3::utils::get_input_config_dir(serial), "custom gamepad configuration")) - RemoveCustomPadConfiguration(serial); - } - - // Remove icons in "Icons/game_icons" folder (if any) - if (content_types & ICONS) - { - if (ValidateRemoval(serial, rpcs3::utils::get_icons_dir(serial), "icons")) - RemoveContentBySerial(rpcs3::utils::get_icons_dir(), serial, "icons"); - } - - // Remove shortcuts in "games/shortcuts" folder and from desktop / start menu (if any) - if (content_types & SHORTCUTS) - { - if (const auto it = m_content_info.name_list.find(serial); it != m_content_info.name_list.cend()) - { - const bool remove_rpcs3_links = ValidateRemoval(serial, rpcs3::utils::get_games_shortcuts_dir(), "link"); - - for (const auto& name : it->second) - { - // Remove illegal characters from name to match the link name created by gui::utils::create_shortcut() - const std::string simple_name = QString::fromStdString(vfs::escape(name, true)).simplified().toStdString(); - - // Remove rpcs3 shortcuts - if (remove_rpcs3_links) - RemoveContentBySerial(rpcs3::utils::get_games_shortcuts_dir(), simple_name + ".lnk", "link"); - - // TODO: Remove shortcuts from desktop/start menu - } - } - } - - if (content_types & SAVESTATES) - { - if (ValidateRemoval(serial, rpcs3::utils::get_savestates_dir(serial), "savestates")) - RemoveContentBySerial(rpcs3::utils::get_savestates_dir(), serial, "savestates"); - } - - if (content_types & CAPTURES) - { - if (ValidateRemoval(serial, rpcs3::utils::get_captures_dir(), "captures")) - RemoveContentBySerial(rpcs3::utils::get_captures_dir(), serial, "captures"); - } - - if (content_types & RECORDINGS) - { - if (ValidateRemoval(serial, rpcs3::utils::get_recordings_dir(serial), "recordings")) - RemoveContentBySerial(rpcs3::utils::get_recordings_dir(), serial, "recordings"); - } - - if (content_types & SCREENSHOTS) - { - if (ValidateRemoval(serial, rpcs3::utils::get_screenshots_dir(serial), "screenshots")) - RemoveContentBySerial(rpcs3::utils::get_screenshots_dir(), serial, "screenshots"); - } - - if (m_content_info.clear_on_finish) - ClearContentList(true); // Update the game list and clear the content's info once removed - - return true; -} - -void game_list_frame::BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, - QString progressLabel, std::function action, - std::function cancel_log, std::function action_on_finish, bool refresh_on_finish, - bool can_be_concurrent, std::function should_wait_cb) -{ - // Concurrent tasks should not wait (at least not in current implementation) - ensure(!should_wait_cb || !can_be_concurrent); - - g_system_progress_canceled = false; - - const std::shared_ptr> iterate_over_serial = std::make_shared>(); - - const std::shared_ptr> index = std::make_shared>(0); - - const int serials_size = ::narrow(serials.size()); - - *iterate_over_serial = [=, this, index_ptr = index](int index) - { - if (index == serials_size) - { - return false; - } - - const std::string& serial = *std::next(serials.begin(), index); - - if (pdlg->wasCanceled() || g_system_progress_canceled.exchange(false)) - { - if (cancel_log) - { - cancel_log(index, serials_size); - } - return false; - } - - if (action(serial)) - { - const int done = index_ptr->load(); - pdlg->setLabelText(progressLabel.arg(done + 1).arg(serials_size)); - pdlg->SetValue(done + 1); - } - - (*index_ptr)++; - return true; - }; - - if (can_be_concurrent) - { - // Unused currently - - QList indices; - - for (int i = 0; i < serials_size; i++) - { - indices.append(i); - } - - QFutureWatcher* future_watcher = new QFutureWatcher(this); - - future_watcher->setFuture(QtConcurrent::map(std::move(indices), *iterate_over_serial)); - - connect(future_watcher, &QFutureWatcher::finished, this, [=, this]() - { - pdlg->setLabelText(progressLabel.arg(index->load()).arg(serials_size)); - pdlg->setCancelButtonText(tr("OK")); - QApplication::beep(); - - if (action_on_finish) - { - action_on_finish(); - } - - if (refresh_on_finish && index) - { - Refresh(true); - } - - future_watcher->deleteLater(); - }); - - return; - } - - const std::shared_ptr> periodic_func = std::make_shared>(); - - *periodic_func = [=, this]() - { - if (should_wait_cb && should_wait_cb()) - { - // Conditions are not met for execution - // Check again later - QTimer::singleShot(5, this, *periodic_func); - return; - } - - if ((*iterate_over_serial)(*index)) - { - QTimer::singleShot(1, this, *periodic_func); - return; - } - - pdlg->setLabelText(progressLabel.arg(index->load()).arg(serials_size)); - pdlg->setCancelButtonText(tr("OK")); - connect(pdlg, &progress_dialog::canceled, this, [pdlg](){ pdlg->deleteLater(); }); - QApplication::beep(); - - if (action_on_finish) - { - action_on_finish(); - } - - // Signal termination back to the callback - action(""); - - if (refresh_on_finish && index) - { - Refresh(true); - } - }; - - // Invoked on the next event loop processing iteration - QTimer::singleShot(1, this, *periodic_func); -} - -void game_list_frame::BatchCreateCPUCaches(const std::vector& games, bool is_fast_compilation, bool is_interactive) -{ - if (is_interactive && QMessageBox::question(this, tr("Confirm Creation"), tr("Create LLVM cache?")) != QMessageBox::Yes) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const usz total = serials.size(); - - if (total == 0) - { - QMessageBox::information(this, tr("LLVM Cache Batch Creation"), tr("No titles found"), QMessageBox::Ok); - return; - } - - if (!m_gui_settings->GetBootConfirmation(this)) - { - return; - } - - const QString main_label = tr("Creating all LLVM caches"); - - progress_dialog* pdlg = new progress_dialog(tr("LLVM Cache Batch Creation"), main_label, tr("Cancel"), 0, ::narrow(total), false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - connect(pdlg, &progress_dialog::canceled, this, []() - { - if (!Emu.IsStopped()) - { - Emu.GracefulShutdown(false, true); - } - }); - - BatchActionBySerials(pdlg, serials, tr("%0\nProgress: %1/%2 caches compiled").arg(main_label), - [&, games](const std::string& serial) - { - if (serial.empty()) - { - return false; - } - - if (Emu.IsStopped(true)) - { - const auto it = std::find_if(m_game_data.begin(), m_game_data.end(), FN(x->info.serial == serial)); - - if (it != m_game_data.end()) - { - return CreateCPUCaches((*it)->info.path, serial, is_fast_compilation); - } - } - - return false; - }, - [this](u32, u32) - { - game_list_log.notice("LLVM Cache Batch Creation was canceled"); - }, nullptr, false, false, - []() - { - return !Emu.IsStopped(true); - }); -} - -void game_list_frame::BatchRemovePPUCaches(const std::vector& games, bool is_interactive) -{ - if (!ValidateBatchRemoval("PPU cache", is_interactive)) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("PPU Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("PPU Cache Batch Removal"), tr("Removing all PPU caches"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 PPU caches cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemovePPUCache(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("PPU Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); - }, nullptr, false); -} - -void game_list_frame::BatchRemoveSPUCaches(const std::vector& games, bool is_interactive) -{ - if (!ValidateBatchRemoval("SPU cache", is_interactive)) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("SPU Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("SPU Cache Batch Removal"), tr("Removing all SPU caches"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 SPU caches cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveSPUCache(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("SPU Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); - }, nullptr, false); -} - -void game_list_frame::BatchRemoveHDD1Caches(const std::vector& games, bool is_interactive) -{ - if (!ValidateBatchRemoval("HDD1 cache", is_interactive)) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("HDD1 Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("HDD1 Cache Batch Removal"), tr("Removing all HDD1 caches"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 HDD1 caches cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveHDD1Cache(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("HDD1 Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); - }, nullptr, false); -} - -void game_list_frame::BatchRemoveAllCaches(const std::vector& games, bool is_interactive) -{ - if (!ValidateBatchRemoval("all caches", is_interactive)) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("Cache Batch Removal"), tr("Removing all caches"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 caches cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveAllCaches(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); - }, nullptr, false); -} - -void game_list_frame::BatchRemoveContentLists(const std::vector& games, bool is_interactive) -{ - // Let the batch process (not RemoveContentList()) make cleanup when terminated - m_content_info.clear_on_finish = false; - - if (!ValidateBatchRemoval("selected content", is_interactive)) - { - ClearContentList(); // Clear only the content's info - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("Content Batch Removal"), tr("No files found"), QMessageBox::Ok); - - ClearContentList(); // Clear only the content's info - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("Content Batch Removal"), tr("Removing all contents"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 contents cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveContentList(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("Content Batch Removal was canceled. %d/%d contents cleared", removed, total); - }, - [this]() // Make cleanup when batch process terminated - { - ClearContentList(true); // Update the game list and clear the content's info once removed - }, false); -} - -void game_list_frame::BatchRemoveCustomConfigurations(const std::vector& games, bool is_interactive) -{ - if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove custom configuration?")) != QMessageBox::Yes) - { - return; - } - - std::set serials; - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - if (game->has_custom_config && !serials.count(game->info.serial)) - { - serials.emplace(game->info.serial); - } - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("Custom Configuration Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("Custom Configuration Batch Removal"), tr("Removing all custom configurations"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 custom configurations cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveCustomConfiguration(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("Custom Configuration Batch Removal was canceled. %d/%d custom configurations cleared", removed, total); - }, nullptr, true); -} - -void game_list_frame::BatchRemoveCustomPadConfigurations(const std::vector& games, bool is_interactive) -{ - if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove custom gamepad configuration?")) != QMessageBox::Yes) - { - return; - } - - std::set serials; - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - if (game->has_custom_pad_config && !serials.count(game->info.serial)) - { - serials.emplace(game->info.serial); - } - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("Custom Gamepad Configuration Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("Custom Gamepad Configuration Batch Removal"), tr("Removing all custom gamepad configurations"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 custom gamepad configurations cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveCustomPadConfiguration(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("Custom Gamepad Configuration Batch Removal was canceled. %d/%d custom gamepad configurations cleared", removed, total); - }, nullptr, true); -} - -void game_list_frame::BatchRemoveShaderCaches(const std::vector& games, bool is_interactive) -{ - if (!ValidateBatchRemoval("shader cache", is_interactive)) - { - return; - } - - std::set serials; - - if (games.empty()) - { - serials.emplace("vsh.self"); - } - - for (const auto& game : (games.empty() ? m_game_data : games)) - { - serials.emplace(game->info.serial); - } - - const u32 total = ::size32(serials); - - if (total == 0) - { - QMessageBox::information(this, tr("Shader Cache Batch Removal"), tr("No files found"), QMessageBox::Ok); - return; - } - - progress_dialog* pdlg = new progress_dialog(tr("Shader Cache Batch Removal"), tr("Removing all shader caches"), tr("Cancel"), 0, total, false, this); - pdlg->setAutoClose(false); - pdlg->setAutoReset(false); - pdlg->open(); - - BatchActionBySerials(pdlg, serials, tr("%0/%1 shader caches cleared"), - [this](const std::string& serial) - { - return !serial.empty() && Emu.IsStopped(true) && RemoveShaderCache(serial); - }, - [this](u32 removed, u32 total) - { - game_list_log.notice("Shader Cache Batch Removal was canceled. %d/%d caches cleared", removed, total); - }, nullptr, false); -} - void game_list_frame::ShowCustomConfigIcon(const game_info& game) { if (!game) @@ -3798,11 +1362,6 @@ void game_list_frame::SetPlayHoverGifs(bool play) } } -const std::vector& game_list_frame::GetGameInfo() const -{ - return m_game_data; -} - void game_list_frame::WaitAndAbortRepaintThreads() { for (const game_info& game : m_game_data) diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index 5de72360e3..332383dae5 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -1,20 +1,18 @@ #pragma once #include "game_list.h" +#include "game_list_actions.h" #include "custom_dock_widget.h" -#include "shortcut_utils.h" #include "Utilities/lockless.h" #include "Utilities/mutex.h" #include "util/auto_typemap.hpp" #include "Emu/config_mode.h" #include -#include #include #include #include #include -#include #include #include @@ -56,61 +54,34 @@ public: void SetShowHidden(bool show); game_compatibility* GetGameCompatibility() const { return m_game_compat; } - - const std::vector& GetGameInfo() const; - - void CreateShortcuts(const std::vector& games, const std::set& locations); + const std::vector& GetGameInfo() const { return m_game_data; } + std::shared_ptr actions() const { return m_game_list_actions; } + std::shared_ptr get_gui_settings() const { return m_gui_settings; } + std::shared_ptr get_emu_settings() const { return m_emu_settings; } + std::shared_ptr get_persistent_settings() const { return m_persistent_settings; } + std::map& notes() { return m_notes; } + std::map& titles() { return m_titles; } + QSet& hidden_list() { return m_hidden_list; } bool IsEntryVisible(const game_info& game, bool search_fallback = false) const; - enum content_type - { - NO_CONTENT = 0, - DISC = (1 << 0), - DATA = (1 << 1), - LOCKS = (1 << 2), - CACHES = (1 << 3), - CUSTOM_CONFIG = (1 << 4), - ICONS = (1 << 5), - SHORTCUTS = (1 << 6), - SAVESTATES = (1 << 7), - CAPTURES = (1 << 8), - RECORDINGS = (1 << 9), - SCREENSHOTS = (1 << 10) - }; + void ShowCustomConfigIcon(const game_info& game); - struct content_info + // Enqueue slot for refreshed signal + // Allowing for an individual container for each distinct use case (currently disabled and contains only one such entry) + template + void AddRefreshedSlot(Func&& func) { - u16 content_types = NO_CONTENT; // Always set by SetContentList() - bool clear_on_finish = true; // Always overridden by BatchRemoveContentLists() + // NOTE: Remove assert when the need for individual containers arises + static_assert(std::is_void_v); - bool is_single_selection = false; - u16 in_games_dir_count = 0; - QString info; - std::map> name_list; - std::map> path_list; - std::set disc_list; - std::set removed_disc_list; // Filled in by RemoveContentList() - }; + connect(this, &game_list_frame::Refreshed, this, [this, func = std::move(func)]() mutable + { + func(m_refresh_funcs_manage_type->get>().m_done_paths); + }, Qt::SingleShotConnection); + } public Q_SLOTS: - void BatchCreateCPUCaches(const std::vector& games = {}, bool is_fast_compilation = false, bool is_interactive = false); - void BatchRemoveCustomConfigurations(const std::vector& games = {}, bool is_interactive = false); - void BatchRemoveCustomPadConfigurations(const std::vector& games = {}, bool is_interactive = false); - void BatchRemoveShaderCaches(const std::vector& games = {}, bool is_interactive = false); - void BatchRemovePPUCaches(const std::vector& games = {}, bool is_interactive = false); - void BatchRemoveSPUCaches(const std::vector& games = {}, bool is_interactive = false); - void BatchRemoveHDD1Caches(const std::vector& games = {}, bool is_interactive = false); - void BatchRemoveAllCaches(const std::vector& games = {}, bool is_interactive = false); - - // NOTES: - // - SetContentList() MUST always be called to set the content's info to be removed by: - // - RemoveContentList() - // - BatchRemoveContentLists() - // - void SetContentList(u16 content_types, const content_info& content_info); - void BatchRemoveContentLists(const std::vector& games = {}, bool is_interactive = false); - void SetListMode(bool is_list); void SetSearchText(const QString& text); void SetShowCompatibilityInGrid(bool show); @@ -128,6 +99,7 @@ private Q_SLOTS: void doubleClickedSlot(QTableWidgetItem* item); void doubleClickedSlot(const game_info& game); void ItemSelectionChangedSlot(); + Q_SIGNALS: void GameListFrameClosed(); void NotifyGameSelection(const game_info& game); @@ -138,7 +110,12 @@ Q_SIGNALS: void Refreshed(); void RequestSaveStateManager(const game_info& game); -public: +protected: + /** Override inherited method from Qt to allow signalling when close happened.*/ + void closeEvent(QCloseEvent* event) override; + bool eventFilter(QObject *object, QEvent *event) override; + +private: template struct GameIdsTable { @@ -146,76 +123,13 @@ public: std::set m_done_paths; }; - // Enqueue slot for refreshed signal - // Allowing for an individual container for each distinct use case (currently disabled and contains only one such entry) - template - void AddRefreshedSlot(Func&& func) - { - // NOTE: Remove assert when the need for individual containers arises - static_assert(std::is_void_v); - - connect(this, &game_list_frame::Refreshed, this, [this, func = std::move(func)]() mutable - { - func(m_refresh_funcs_manage_type->get>().m_done_paths); - }, Qt::SingleShotConnection); - } - -protected: - /** Override inherited method from Qt to allow signalling when close happened.*/ - void closeEvent(QCloseEvent* event) override; - bool eventFilter(QObject *object, QEvent *event) override; -private: void push_path(const std::string& path, std::vector& legit_paths); QString get_header_text(int col) const; QString get_action_text(int col) const; - void ShowCustomConfigIcon(const game_info& game); bool SearchMatchesApp(const QString& name, const QString& serial, bool fallback = false) const; - void ShowSingleSelectionContextMenu(const game_info& gameinfo, QPoint& global_pos); - void ShowMultiSelectionContextMenu(const std::vector& games, QPoint& global_pos); - - // NOTE: - // m_content_info is used by: - // - SetContentList() - // - ClearContentList() - // - GetContentInfo() - // - RemoveContentList() - // - BatchRemoveContentLists() - // - content_info m_content_info; - - void ClearContentList(bool refresh = false); - content_info GetContentInfo(const std::vector& games); - - void ShowRemoveGameDialog(const std::vector& games); - void ShowGameInfoDialog(const std::vector& games); - - static bool IsGameRunning(const std::string& serial); - bool ValidateRemoval(const std::string& serial, const std::string& path, const std::string& desc, bool is_interactive = false); - bool ValidateBatchRemoval(const std::string& desc, bool is_interactive = false); - - static bool CreateCPUCaches(const std::string& path, const std::string& serial = {}, bool is_fast_compilation = false); - static bool CreateCPUCaches(const game_info& game, bool is_fast_compilation = false); - bool RemoveCustomConfiguration(const std::string& serial, const game_info& game = nullptr, bool is_interactive = false); - bool RemoveCustomPadConfiguration(const std::string& serial, const game_info& game = nullptr, bool is_interactive = false); - bool RemoveShaderCache(const std::string& serial, bool is_interactive = false); - bool RemovePPUCache(const std::string& serial, bool is_interactive = false); - bool RemoveSPUCache(const std::string& serial, bool is_interactive = false); - bool RemoveHDD1Cache(const std::string& serial, bool is_interactive = false); - bool RemoveAllCaches(const std::string& serial, bool is_interactive = false); - bool RemoveContentList(const std::string& serial, bool is_interactive = false); - - static bool RemoveContentPath(const std::string& path, const std::string& desc); - static u32 RemoveContentPathList(const std::set& path_list, const std::string& desc); - static bool RemoveContentBySerial(const std::string& base_dir, const std::string& serial, const std::string& desc); - - void BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, - QString progressLabel, std::function action, - std::function cancel_log, std::function action_on_finish, bool refresh_on_finish, - bool can_be_concurrent = false, std::function should_wait_cb = {}); - std::string CurrentSelectionPath(); game_info GetGameInfoByMode(const QTableWidgetItem* item) const; @@ -224,6 +138,8 @@ private: void WaitAndAbortRepaintThreads(); void WaitAndAbortSizeCalcThreads(); + std::shared_ptr m_game_list_actions; + // Which widget we are displaying depends on if we are in grid or list mode. QMainWindow* m_game_dock = nullptr; QStackedWidget* m_central_widget = nullptr; diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index ca7b07bb0f..0fc48c412e 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -3,10 +3,8 @@ #include "game_list_grid_item.h" #include "gui_settings.h" #include "qt_utils.h" -#include "Utilities/File.h" #include -#include game_list_grid::game_list_grid() : flow_widget(nullptr), game_list_base() diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 6132e237f8..485f7e3f22 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2401,7 +2401,7 @@ void main_window::CreateShortCuts(const std::map& paths, b if (!game_data_shortcuts.empty() && !locations.empty()) { - m_game_list_frame->CreateShortcuts(game_data_shortcuts, locations); + m_game_list_frame->actions()->CreateShortcuts(game_data_shortcuts, locations); } } } @@ -2428,7 +2428,7 @@ void main_window::PrecompileCachesFromInstalledPackages(const std::mapBatchCreateCPUCaches(game_data, true); + m_game_list_frame->actions()->BatchCreateCPUCaches(game_data, true); } } @@ -2724,40 +2724,40 @@ void main_window::CreateConnects() connect(ui->batchCreateCPUCachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchCreateCPUCaches({}, false, true); + m_game_list_frame->actions()->BatchCreateCPUCaches({}, false, true); }); connect(ui->batchRemoveCustomConfigurationsAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveCustomConfigurations({}, true); + m_game_list_frame->actions()->BatchRemoveCustomConfigurations({}, true); }); connect(ui->batchRemoveCustomPadConfigurationsAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveCustomPadConfigurations({}, true); + m_game_list_frame->actions()->BatchRemoveCustomPadConfigurations({}, true); }); connect(ui->batchRemoveShaderCachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveShaderCaches({}, true); + m_game_list_frame->actions()->BatchRemoveShaderCaches({}, true); }); connect(ui->batchRemovePPUCachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemovePPUCaches({}, true); + m_game_list_frame->actions()->BatchRemovePPUCaches({}, true); }); connect(ui->batchRemoveSPUCachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveSPUCaches({}, true); + m_game_list_frame->actions()->BatchRemoveSPUCaches({}, true); }); connect(ui->removeHDD1CachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveHDD1Caches({}, true); + m_game_list_frame->actions()->BatchRemoveHDD1Caches({}, true); }); connect(ui->removeAllCachesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->BatchRemoveAllCaches({}, true); + m_game_list_frame->actions()->BatchRemoveAllCaches({}, true); }); connect(ui->removeSavestatesAct, &QAction::triggered, this, [this]() { - m_game_list_frame->SetContentList(game_list_frame::content_type::SAVESTATES, {}); - m_game_list_frame->BatchRemoveContentLists({}, true); + m_game_list_frame->actions()->SetContentList(game_list_actions::content_type::SAVESTATES, {}); + m_game_list_frame->actions()->BatchRemoveContentLists({}, true); }); connect(ui->cleanUpGameListAct, &QAction::triggered, this, &main_window::CleanUpGameList);