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