diff --git a/.ci/deploy-windows-clang-cl.sh b/.ci/deploy-windows-clang-cl.sh
index 51742e2790..77c2071a82 100644
--- a/.ci/deploy-windows-clang-cl.sh
+++ b/.ci/deploy-windows-clang-cl.sh
@@ -25,6 +25,9 @@ mkdir ./bin/config/input_configs
curl -fsSL 'https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt' 1> ./bin/config/input_configs/gamecontrollerdb.txt
curl -fsSL 'https://rpcs3.net/compatibility?api=v1&export' | iconv -t UTF-8 1> ./bin/GuiConfigs/compat_database.dat
+# Set Qt plugin & translation path
+cp D:/a/rpcs3/rpcs3/rpcs3/qt/etc/qt.conf ./bin/qt.conf
+
# Download translations
mkdir -p ./bin/qt6/translations
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml
index 8fff8055b9..108ef432f2 100644
--- a/.github/workflows/rpcs3.yml
+++ b/.github/workflows/rpcs3.yml
@@ -373,8 +373,6 @@ jobs:
mingw-w64-clang-x86_64-qt6-declarative
mingw-w64-clang-x86_64-qt6-multimedia
mingw-w64-clang-x86_64-qt6-svg
- mingw-w64-clang-x86_64-qt6-tools
- mingw-w64-clang-x86_64-qt6-translations
base-devel
curl
git
diff --git a/3rdparty/libsdl-org/SDL b/3rdparty/libsdl-org/SDL
index a962f40bbb..f5e5f65889 160000
--- a/3rdparty/libsdl-org/SDL
+++ b/3rdparty/libsdl-org/SDL
@@ -1 +1 @@
-Subproject commit a962f40bbba175e9716557a25d5d7965f134a3d3
+Subproject commit f5e5f6588921eed3d7d048ce43d9eb1ff0da0ffc
diff --git a/3rdparty/libsdl-org/SDL.vcxproj b/3rdparty/libsdl-org/SDL.vcxproj
index fd2bcf2f03..f0b38ca09f 100644
--- a/3rdparty/libsdl-org/SDL.vcxproj
+++ b/3rdparty/libsdl-org/SDL.vcxproj
@@ -23,7 +23,6 @@
-
@@ -103,7 +102,6 @@
-
@@ -132,8 +130,6 @@
-
-
@@ -144,11 +140,7 @@
-
-
-
-
@@ -164,7 +156,6 @@
-
@@ -184,6 +175,7 @@
+
@@ -192,35 +184,20 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -264,7 +241,6 @@
-
@@ -280,14 +256,13 @@
-
-
+
@@ -328,6 +303,7 @@
+
@@ -357,6 +333,7 @@
+
@@ -416,6 +393,7 @@
+
@@ -486,7 +464,6 @@
-
@@ -494,11 +471,12 @@
+
-
+
diff --git a/3rdparty/libsdl-org/SDL.vcxproj.filters b/3rdparty/libsdl-org/SDL.vcxproj.filters
index 8b7f293ef3..5839899c0d 100644
--- a/3rdparty/libsdl-org/SDL.vcxproj.filters
+++ b/3rdparty/libsdl-org/SDL.vcxproj.filters
@@ -214,9 +214,6 @@
{000028b2ea36d7190d13777a4dc70000}
-
- {695ffc61-5497-4227-b415-15e9bdd5b6bf}
-
@@ -702,6 +699,9 @@
video\yuv2rgb
+
+ video\windows
+
video\windows
@@ -831,6 +831,9 @@
render\software
+
+ render\software
+
render\software
@@ -908,6 +911,12 @@
+
+
+
+
+
+
render\vulkan
@@ -941,60 +950,6 @@
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video
-
-
- video
-
-
- video
-
-
- misc
-
-
- haptic\hidapi
-
-
- haptic\hidapi
-
-
- core
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- API Headers
-
@@ -1082,6 +1037,9 @@
core
+
+ core\windows
+
core\windows
@@ -1208,6 +1166,9 @@
joystick\dummy
+
+ joystick\gdk
+
joystick\hidapi
@@ -1367,6 +1328,9 @@
video\dummy
+
+ video\windows
+
video\windows
@@ -1379,6 +1343,9 @@
video\windows
+
+ video\windows
+
video\windows
@@ -1541,6 +1508,9 @@
render\software
+
+ render\software
+
render\software
@@ -1565,6 +1535,9 @@
+
+
+
render\vulkan
@@ -1606,66 +1579,6 @@
-
- video\windows
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video\yuv2rgb
-
-
- video
-
-
- misc
-
-
- haptic\hidapi
-
-
- haptic\hidapi
-
-
- core\windows
-
-
- core\windows
-
-
- joystick\gdk
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
-
- joystick\hidapi
-
diff --git a/BUILDING.md b/BUILDING.md
index 32cdc6cc03..26295d0a16 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -51,7 +51,7 @@ These are the essentials tools to build RPCS3 on Linux. Some of them can be inst
#### Debian & Ubuntu
- sudo apt-get install build-essential ninja-build libasound2-dev libpulse-dev libopenal-dev libglew-dev zlib1g-dev libedit-dev libvulkan-dev libudev-dev git libevdev-dev libsdl3-3.2 libsdl3-dev libjack-dev libsndio-dev
+ sudo apt-get install build-essential ninja-build libasound2-dev libpulse-dev libopenal-dev libglew-dev zlib1g-dev libedit-dev libvulkan-dev libudev-dev git libevdev-dev libsdl3-dev libjack-dev libsndio-dev libcurl4-openssl-dev qt6-base-dev qt6-base-private-dev qt6-multimedia-dev qt6-svg-dev libxkbcommon-dev
Ubuntu is usually horrendously out of date, and some packages need to be downloaded by hand. This part is for Qt, GCC, Vulkan, and CMake
diff --git a/Utilities/File.cpp b/Utilities/File.cpp
index aafcfe3c30..30e6414675 100644
--- a/Utilities/File.cpp
+++ b/Utilities/File.cpp
@@ -901,6 +901,22 @@ std::string_view fs::get_parent_dir_view(std::string_view path, u32 parent_level
return result;
}
+std::string fs::get_path_if_dir(const std::string& path)
+{
+ if (path.empty() || !fs::is_dir(path))
+ {
+ return {};
+ }
+
+ // If delimiters are already present at the end of the string then nothing else to do
+ if (usz sz = path.find_last_of(delim); sz != umax && (sz + 1) == path.size())
+ {
+ return path;
+ }
+
+ return path + '/';
+}
+
bool fs::get_stat(const std::string& path, stat_t& info)
{
// Ensure consistent information on failure
diff --git a/Utilities/File.h b/Utilities/File.h
index a5a4c53d75..90453f16f0 100644
--- a/Utilities/File.h
+++ b/Utilities/File.h
@@ -195,6 +195,9 @@ namespace fs
return std::string{get_parent_dir_view(path, parent_level)};
}
+ // Return "path" plus an ending delimiter (if missing) if "path" is an existing directory. Otherwise, an empty string
+ std::string get_path_if_dir(const std::string& path);
+
// Get file information
bool get_stat(const std::string& path, stat_t& info);
diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp
index 463ec7a68d..3afa2a9e06 100644
--- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp
+++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp
@@ -137,6 +137,7 @@ public:
const std::array& get_new_location();
void connect_usb_device(std::shared_ptr dev, bool update_usb_devices = false);
void disconnect_usb_device(std::shared_ptr dev, bool update_usb_devices = false);
+ void reconnect_usb_device(u32 assigned_number);
// Map of devices actively handled by the ps3(device_id, device)
std::map>> handled_devices;
@@ -205,19 +206,23 @@ private:
{0x1BAD, 0x3430, 0x343F, "Harmonix Button Guitar - Wii", nullptr, nullptr},
{0x1BAD, 0x3530, 0x353F, "Harmonix Real Guitar - Wii", nullptr, nullptr},
- //Top Shot Elite controllers
+ // Top Shot Elite controllers
{0x12BA, 0x04A0, 0x04A0, "Top Shot Elite", nullptr, nullptr},
{0x12BA, 0x04A1, 0x04A1, "Top Shot Fearmaster", nullptr, nullptr},
{0x12BA, 0x04B0, 0x04B0, "Rapala Fishing Rod", nullptr, nullptr},
- // GT5 Wheels&co
+ // Wheels
#ifdef HAVE_SDL3
{0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b", &usb_device_logitech_g27::get_num_emu_devices, &usb_device_logitech_g27::make_instance},
#else
{0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b", nullptr, nullptr},
#endif
+ {0x046D, 0xCA03, 0xCA03, "lgFF_ca03_ca03", nullptr, nullptr},
+ {0x044F, 0xB652, 0xB652, "Thrustmaster FGT FFB old", nullptr, nullptr},
{0x044F, 0xB653, 0xB653, "Thrustmaster RGT FFB Pro", nullptr, nullptr},
+ {0x044F, 0xB654, 0xB654, "Thrustmaster FGT FFB", nullptr, nullptr},
+ {0x044F, 0xb655, 0xb655, "Thrustmaster FGT Rumble 3-in-1", nullptr, nullptr},
{0x044F, 0xB65A, 0xB65A, "Thrustmaster F430", nullptr, nullptr},
{0x044F, 0xB65D, 0xB65D, "Thrustmaster FFB", nullptr, nullptr},
{0x044F, 0xB65E, 0xB65E, "Thrustmaster TRS", nullptr, nullptr},
@@ -225,7 +230,6 @@ private:
// GT6
{0x2833, 0x0001, 0x0001, "Oculus", nullptr, nullptr},
- {0x046D, 0xCA03, 0xCA03, "lgFF_ca03_ca03", nullptr, nullptr},
// Buzz controllers
{0x054C, 0x1000, 0x1040, "buzzer0", &usb_device_buzz::get_num_emu_devices, &usb_device_buzz::make_instance},
@@ -968,6 +972,21 @@ void usb_handler_thread::disconnect_usb_device(std::shared_ptr dev,
}
}
+void usb_handler_thread::reconnect_usb_device(u32 assigned_number)
+{
+ std::lock_guard lock(mutex);
+ ensure(assigned_number != 0);
+ for (const auto& dev : usb_devices)
+ {
+ if (dev->assigned_number == assigned_number)
+ {
+ disconnect_usb_device(dev, false);
+ connect_usb_device(dev, false);
+ break;
+ }
+ }
+}
+
void connect_usb_controller(u8 index, input::product_type type)
{
auto usbh = g_fxo->try_get>();
@@ -1061,18 +1080,7 @@ void reconnect_usb(u32 assigned_number)
{
return;
}
-
- std::lock_guard lock(usbh->mutex);
- for (auto& [nr, pair] : usbh->handled_devices)
- {
- auto& [internal_dev, dev] = pair;
- if (nr == assigned_number)
- {
- usbh->disconnect_usb_device(dev, false);
- usbh->connect_usb_device(dev, false);
- break;
- }
- }
+ usbh->reconnect_usb_device(assigned_number);
}
void handle_hotplug_event(bool connected)
diff --git a/rpcs3/Emu/Io/LogitechG27.cpp b/rpcs3/Emu/Io/LogitechG27.cpp
index c8aa453592..2eb62581bb 100644
--- a/rpcs3/Emu/Io/LogitechG27.cpp
+++ b/rpcs3/Emu/Io/LogitechG27.cpp
@@ -1752,7 +1752,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
if (!SDL_RunHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, 1))
{
- logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError());
+ logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s", m_effect_slots[i].last_effect.type, i, SDL_GetError());
}
}
else
@@ -1796,14 +1796,14 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
if (!SDL_RunHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, 1))
{
- logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError());
+ logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s", m_effect_slots[i].last_effect.type, i, SDL_GetError());
}
}
else
{
if (!SDL_StopHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id))
{
- logitech_g27_log.error("Failed stopping sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError());
+ logitech_g27_log.error("Failed stopping sdl effect %d on slot %d, %s", m_effect_slots[i].last_effect.type, i, SDL_GetError());
}
}
}
@@ -1824,11 +1824,11 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
if (cmd == 0x02)
{
- logitech_g27_log.error("Tried to play effect slot %d but it was never uploaded\n", i);
+ logitech_g27_log.error("Tried to play effect slot %d but it was never uploaded", i);
}
else
{
- logitech_g27_log.error("Tried to stop effect slot %d but it was never uploaded\n", i);
+ logitech_g27_log.error("Tried to stop effect slot %d but it was never uploaded", i);
}
}
}
diff --git a/rpcs3/Emu/system_utils.cpp b/rpcs3/Emu/system_utils.cpp
index 3cf886d06d..1d94b22987 100644
--- a/rpcs3/Emu/system_utils.cpp
+++ b/rpcs3/Emu/system_utils.cpp
@@ -149,11 +149,6 @@ namespace rpcs3::utils
return emu_dir_.empty() ? fs::get_config_dir() : emu_dir_;
}
- std::string get_games_dir()
- {
- return g_cfg_vfs.get(g_cfg_vfs.games_dir, get_emu_dir());
- }
-
std::string get_hdd0_dir()
{
return g_cfg_vfs.get(g_cfg_vfs.dev_hdd0, get_emu_dir());
@@ -184,11 +179,31 @@ namespace rpcs3::utils
return g_cfg_vfs.get(g_cfg_vfs.dev_bdvd, get_emu_dir());
}
+ std::string get_games_dir()
+ {
+ return g_cfg_vfs.get(g_cfg_vfs.games_dir, get_emu_dir());
+ }
+
std::string get_hdd0_game_dir()
{
return get_hdd0_dir() + "game/";
}
+ std::string get_hdd0_locks_dir()
+ {
+ return get_hdd0_game_dir() + "$locks/";
+ }
+
+ std::string get_hdd1_cache_dir()
+ {
+ return get_hdd1_dir() + "caches/";
+ }
+
+ std::string get_games_shortcuts_dir()
+ {
+ return get_games_dir() + "shortcuts/";
+ }
+
u64 get_cache_disk_usage()
{
if (const u64 data_size = fs::get_dir_size(rpcs3::utils::get_cache_dir(), 1); data_size != umax)
@@ -226,6 +241,98 @@ namespace rpcs3::utils
return cache_dir;
}
+ std::string get_data_dir()
+ {
+ return fs::get_config_dir() + "data/";
+ }
+
+ std::string get_icons_dir()
+ {
+ return fs::get_config_dir() + "Icons/game_icons/";
+ }
+
+ std::string get_savestates_dir()
+ {
+ return fs::get_config_dir() + "savestates/";
+ }
+
+ std::string get_captures_dir()
+ {
+ return fs::get_config_dir() + "captures/";
+ }
+
+ std::string get_recordings_dir()
+ {
+ return fs::get_config_dir() + "recordings/";
+ }
+
+ std::string get_screenshots_dir()
+ {
+ return fs::get_config_dir() + "screenshots/";
+ }
+
+ std::string get_cache_dir_by_serial(const std::string& serial)
+ {
+ return get_cache_dir() + (serial == "vsh.self" ? "vsh" : serial);
+ }
+
+ std::string get_data_dir(const std::string& serial)
+ {
+ return get_data_dir() + serial;
+ }
+
+ std::string get_icons_dir(const std::string& serial)
+ {
+ return get_icons_dir() + serial;
+ }
+
+ std::string get_savestates_dir(const std::string& serial)
+ {
+ return get_savestates_dir() + serial;
+ }
+
+ std::string get_recordings_dir(const std::string& serial)
+ {
+ return get_recordings_dir() + serial;
+ }
+
+ std::string get_screenshots_dir(const std::string& serial)
+ {
+ return get_screenshots_dir() + serial;
+ }
+
+ std::set get_dir_list(const std::string& base_dir, const std::string& serial)
+ {
+ std::set dir_list;
+
+ for (const auto& entry : fs::dir(base_dir))
+ {
+ // Check for sub folder starting with serial (e.g. BCES01118_BCES01118)
+ if (entry.is_directory && entry.name.starts_with(serial))
+ {
+ dir_list.insert(base_dir + entry.name);
+ }
+ }
+
+ return dir_list;
+ }
+
+ std::set get_file_list(const std::string& base_dir, const std::string& serial)
+ {
+ std::set file_list;
+
+ for (const auto& entry : fs::dir(base_dir))
+ {
+ // Check for files starting with serial (e.g. BCES01118_BCES01118)
+ if (!entry.is_directory && entry.name.starts_with(serial))
+ {
+ file_list.insert(base_dir + entry.name);
+ }
+ }
+
+ return file_list;
+ }
+
std::string get_rap_file_path(const std::string_view& rap)
{
const std::string home_dir = get_hdd0_dir() + "home";
diff --git a/rpcs3/Emu/system_utils.hpp b/rpcs3/Emu/system_utils.hpp
index c2825abb24..d8f4f59d8a 100644
--- a/rpcs3/Emu/system_utils.hpp
+++ b/rpcs3/Emu/system_utils.hpp
@@ -2,6 +2,7 @@
#include "util/types.hpp"
#include
+#include
enum class game_content_type
{
@@ -26,21 +27,42 @@ namespace rpcs3::utils
// VFS directories and disk usage
std::vector> get_vfs_disk_usage();
std::string get_emu_dir();
- std::string get_games_dir();
std::string get_hdd0_dir();
std::string get_hdd1_dir();
std::string get_flash_dir();
std::string get_flash2_dir();
std::string get_flash3_dir();
std::string get_bdvd_dir();
+ std::string get_games_dir();
std::string get_hdd0_game_dir();
+ std::string get_hdd0_locks_dir();
+ std::string get_hdd1_cache_dir();
+ std::string get_games_shortcuts_dir();
// Cache directories and disk usage
u64 get_cache_disk_usage();
std::string get_cache_dir();
std::string get_cache_dir(std::string_view module_path);
+ std::string get_data_dir();
+ std::string get_icons_dir();
+ std::string get_savestates_dir();
+ std::string get_captures_dir();
+ std::string get_recordings_dir();
+ std::string get_screenshots_dir();
+
+ // get_cache_dir_by_serial() named in this way to avoid conflict (wrong invocation) with get_cache_dir()
+ std::string get_cache_dir_by_serial(const std::string& serial);
+ std::string get_data_dir(const std::string& serial);
+ std::string get_icons_dir(const std::string& serial);
+ std::string get_savestates_dir(const std::string& serial);
+ std::string get_recordings_dir(const std::string& serial);
+ std::string get_screenshots_dir(const std::string& serial);
+
+ std::set get_dir_list(const std::string& base_dir, const std::string& serial);
+ std::set get_file_list(const std::string& base_dir, const std::string& serial);
+
std::string get_rap_file_path(const std::string_view& rap);
bool verify_c00_unlock_edat(const std::string_view& content_id, bool fast = false);
std::string get_sfo_dir_from_game_path(const std::string& game_path, const std::string& title_id = "");
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 aad7ab5dbf..a8fc4c5886 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/cg_disasm_window.cpp b/rpcs3/rpcs3qt/cg_disasm_window.cpp
index 480383b973..87aff89546 100644
--- a/rpcs3/rpcs3qt/cg_disasm_window.cpp
+++ b/rpcs3/rpcs3qt/cg_disasm_window.cpp
@@ -146,6 +146,7 @@ void cg_disasm_window::dropEvent(QDropEvent* ev)
{
if (IsValidFile(*ev->mimeData(), true))
{
+ ev->acceptProposedAction();
ShowDisasm();
}
}
@@ -154,7 +155,7 @@ void cg_disasm_window::dragEnterEvent(QDragEnterEvent* ev)
{
if (IsValidFile(*ev->mimeData()))
{
- ev->accept();
+ ev->acceptProposedAction();
}
}
@@ -162,11 +163,6 @@ void cg_disasm_window::dragMoveEvent(QDragMoveEvent* ev)
{
if (IsValidFile(*ev->mimeData()))
{
- ev->accept();
+ ev->acceptProposedAction();
}
}
-
-void cg_disasm_window::dragLeaveEvent(QDragLeaveEvent* ev)
-{
- ev->accept();
-}
diff --git a/rpcs3/rpcs3qt/cg_disasm_window.h b/rpcs3/rpcs3qt/cg_disasm_window.h
index dc0a963c84..97aeee54f8 100644
--- a/rpcs3/rpcs3qt/cg_disasm_window.h
+++ b/rpcs3/rpcs3qt/cg_disasm_window.h
@@ -36,5 +36,4 @@ protected:
void dropEvent(QDropEvent* ev) override;
void dragEnterEvent(QDragEnterEvent* ev) override;
void dragMoveEvent(QDragMoveEvent* ev) override;
- void dragLeaveEvent(QDragLeaveEvent* ev) override;
};
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_base.h b/rpcs3/rpcs3qt/game_list_base.h
index 6d25aad3b3..2af79b0b01 100644
--- a/rpcs3/rpcs3qt/game_list_base.h
+++ b/rpcs3/rpcs3qt/game_list_base.h
@@ -15,7 +15,7 @@ public:
[[maybe_unused]] const std::vector& game_data,
[[maybe_unused]] const std::map& notes_map,
[[maybe_unused]] const std::map& title_map,
- [[maybe_unused]] const std::string& selected_item_id,
+ [[maybe_unused]] const std::set& selected_item_ids,
[[maybe_unused]] bool play_hover_movies){};
void set_icon_size(QSize size) { m_icon_size = std::move(size); }
diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp
new file mode 100644
index 0000000000..bb15bc80ec
--- /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);
+ });
+
+ 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 677b61a44c..f8b48e8508 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,7 +10,6 @@
#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"
@@ -21,39 +18,29 @@
#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();
@@ -325,105 +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::vector& 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;
-}
-
-std::vector game_list_frame::GetDirListBySerial(const std::string& base_dir, const std::string& serial)
-{
- std::vector dir_list;
-
- for (const auto& entry : fs::dir(base_dir))
- {
- // Check for sub folder starting with serial (e.g. BCES01118_BCES01118)
- if (entry.is_directory && entry.name.starts_with(serial))
- {
- dir_list.push_back(base_dir + entry.name);
- }
- }
-
- return dir_list;
-}
-
-std::string game_list_frame::GetCacheDirBySerial(const std::string& serial)
-{
- return rpcs3::utils::get_cache_dir() + (serial == "vsh.self" ? "vsh" : serial);
-}
-
-std::string game_list_frame::GetDataDirBySerial(const std::string& serial)
-{
- return fs::get_config_dir() + "data/" + serial;
-}
-
void game_list_frame::push_path(const std::string& path, std::vector& legit_paths)
{
{
@@ -558,7 +446,7 @@ void game_list_frame::Refresh(const bool from_drive, const std::vector selected_items = CurrentSelectionPaths();
// Release old data
for (const auto& game : m_game_data)
@@ -593,7 +481,7 @@ void game_list_frame::Refresh(const bool from_drive, const std::vectorclear_list();
const int scroll_position = m_game_list->verticalScrollBar()->value();
- m_game_list->populate(matching_apps, m_notes, m_titles, selected_item, m_play_hover_movies);
+ m_game_list->populate(matching_apps, m_notes, m_titles, selected_items, m_play_hover_movies);
m_game_list->sort(m_game_data.size(), m_sort_column, m_col_sort_order);
RepaintIcons();
@@ -609,7 +497,7 @@ void game_list_frame::Refresh(const bool from_drive, const std::vectorclear_list();
- m_game_grid->populate(matching_apps, m_notes, m_titles, selected_item, m_play_hover_movies);
+ m_game_grid->populate(matching_apps, m_notes, m_titles, selected_items, m_play_hover_movies);
RepaintIcons();
}
}
@@ -1068,1676 +956,44 @@ 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::ShowContextMenu(const QPoint& pos)
{
QPoint global_pos;
- game_info gameinfo;
+ std::vector games;
+
+ // NOTE: Currently, only m_game_list supports rows multi selection!
+ //
+ // TODO: Add support to rows multi selection to m_game_grid
if (m_is_list_layout)
{
- QTableWidgetItem* item = m_game_list->item(m_game_list->indexAt(pos).row(), static_cast(gui::game_list_columns::icon));
global_pos = m_game_list->viewport()->mapToGlobal(pos);
- gameinfo = GetGameInfoFromItem(item);
+
+ const auto item_list = m_game_list->selectedItems();
+ game_info gameinfo;
+
+ for (const auto& item : item_list)
+ {
+ if (item->column() != static_cast(gui::game_list_columns::icon))
+ continue;
+
+ if (gameinfo = GetGameInfoFromItem(item); gameinfo)
+ games.push_back(gameinfo);
+ }
}
else if (game_list_grid_item* item = static_cast(m_game_grid->selected_item()))
{
- gameinfo = item->game();
global_pos = m_game_grid->mapToGlobal(pos);
+
+ if (game_info gameinfo = item->game(); gameinfo)
+ games.push_back(gameinfo);
}
- if (!gameinfo)
+ if (!games.empty())
{
- return;
+ game_list_context_menu menu(this);
+ menu.show_menu(games, global_pos);
}
-
- GameInfo current_game = gameinfo->info;
- const QString serial = QString::fromStdString(current_game.serial);
- const QString name = QString::fromStdString(current_game.name).simplified();
-
- const std::string cache_base_dir = GetCacheDirBySerial(current_game.serial);
- const std::string config_data_base_dir = GetDataDirBySerial(current_game.serial);
-
- // Make Actions
- QMenu menu;
-
- static const auto is_game_running = [](const std::string& serial)
- {
- return !Emu.IsStopped(true) && (serial == Emu.GetTitleID() || (serial == "vsh.self" && Emu.IsVsh()));
- };
-
- const bool is_current_running_game = is_game_running(current_game.serial);
-
- 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(current_game.serial, current_game.path, 1); is_savestate_compatible(sstate))
- {
- const bool has_ambiguity = !get_savestate_file(current_game.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();
-
- 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, current_game, gameinfo]()
- {
- if (RemoveCustomConfiguration(current_game.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, current_game, gameinfo]()
- {
- if (RemoveCustomPadConfiguration(current_game.serial, gameinfo, true))
- {
- ShowCustomConfigIcon(gameinfo);
- }
- });
- }
-
- const bool has_cache_dir = fs::is_dir(cache_base_dir);
-
- if (has_cache_dir)
- {
- remove_menu->addSeparator();
-
- QAction* remove_shaders_cache = remove_menu->addAction(tr("&Remove Shaders Cache"));
- remove_shaders_cache->setEnabled(!is_current_running_game);
- connect(remove_shaders_cache, &QAction::triggered, [this, cache_base_dir]()
- {
- RemoveShadersCache(cache_base_dir, 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, cache_base_dir]()
- {
- RemovePPUCache(cache_base_dir, 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, cache_base_dir]()
- {
- RemoveSPUCache(cache_base_dir, true);
- });
- }
-
- const std::string hdd1_cache_base_dir = rpcs3::utils::get_hdd1_dir() + "caches/";
- const bool has_hdd1_cache_dir = !GetDirListBySerial(hdd1_cache_base_dir, current_game.serial).empty();
-
- 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, hdd1_cache_base_dir, serial = current_game.serial]()
- {
- RemoveHDD1Cache(hdd1_cache_base_dir, serial, true);
- });
- }
-
- if (has_cache_dir || 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, current_game, cache_base_dir, hdd1_cache_base_dir]()
- {
- if (is_game_running(current_game.serial))
- return;
-
- if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove all caches?")) != QMessageBox::Yes)
- return;
-
- RemoveContentPath(cache_base_dir, "cache");
- RemoveHDD1Cache(hdd1_cache_base_dir, current_game.serial);
- });
- }
-
- const std::string savestate_dir = fs::get_config_dir() + "savestates/" + current_game.serial;
-
- if (fs::is_dir(savestate_dir))
- {
- remove_menu->addSeparator();
-
- QAction* remove_savestate = remove_menu->addAction(tr("&Remove Savestates"));
- remove_savestate->setEnabled(!is_current_running_game);
- connect(remove_savestate, &QAction::triggered, [this, current_game, savestate_dir]()
- {
- if (is_game_running(current_game.serial))
- return;
-
- if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove savestates?")) != QMessageBox::Yes)
- return;
-
- RemoveContentPath(savestate_dir, "savestate");
- });
- }
-
- // 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 From Game List"));
- hide_serial->setCheckable(true);
- hide_serial->setChecked(m_hidden_list.contains(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);
-
- // 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 = fs::get_config_dir() + "/Icons/game_icons/" + current_game.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(serial);
- break;
- case icon_type::hover_gif:
- msg = tr("Remove Custom Hover Gif of %0?").arg(serial);
- break;
- case icon_type::shader_load:
- msg = tr("Remove Custom Shader Loading Background of %0?").arg(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 captures_dir = fs::get_config_dir() + "/captures/";
- const std::string recordings_dir = fs::get_config_dir() + "/recordings/" + current_game.serial;
- const std::string screenshots_dir = fs::get_config_dir() + "/screenshots/" + current_game.serial;
- std::vector 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);
- });
-
- data_dir_list = GetDirListBySerial(rpcs3::utils::get_hdd0_dir() + "game/", current_game.serial); // It could be absent for a disc game
- }
- else
- {
- data_dir_list.push_back(current_game.path);
- }
-
- if (!data_dir_list.empty()) // "true" if data path is present (it could be absent 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_dir = open_folder_menu->addAction(tr("&Open Custom Config Folder"));
- connect(open_config_dir, &QAction::triggered, [current_game]()
- {
- const std::string config_path = rpcs3::utils::get_custom_config_path(current_game.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() && has_cache_dir)
- {
- 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 (fs::is_dir(config_data_base_dir))
- {
- QAction* open_config_data_dir = open_folder_menu->addAction(tr("&Open Config Data Folder"));
- connect(open_config_data_dir, &QAction::triggered, [config_data_base_dir]()
- {
- gui::utils::open_dir(config_data_base_dir);
- });
- }
-
- if (fs::is_dir(savestate_dir))
- {
- QAction* open_savestate_dir = open_folder_menu->addAction(tr("&Open Savestate Folder"));
- connect(open_savestate_dir, &QAction::triggered, [savestate_dir]()
- {
- gui::utils::open_dir(savestate_dir);
- });
- }
-
- QAction* open_captures_dir = open_folder_menu->addAction(tr("&Open Captures Folder"));
- connect(open_captures_dir, &QAction::triggered, [captures_dir]()
- {
- gui::utils::open_dir(captures_dir);
- });
-
- if (fs::is_dir(recordings_dir))
- {
- QAction* open_recordings_dir = open_folder_menu->addAction(tr("&Open Recordings Folder"));
- connect(open_recordings_dir, &QAction::triggered, [recordings_dir]()
- {
- gui::utils::open_dir(recordings_dir);
- });
- }
-
- if (fs::is_dir(screenshots_dir))
- {
- QAction* open_screenshots_dir = open_folder_menu->addAction(tr("&Open Screenshots Folder"));
- connect(open_screenshots_dir, &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, 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, current_game, gameinfo, cache_base_dir, hdd1_cache_base_dir, name]
- {
- if (is_game_running(current_game.serial))
- {
- QMessageBox::critical(this, tr("Cannot Remove Game"), tr("The PS3 application is still running, it cannot be removed!"));
- return;
- }
-
- const bool is_disc_game = QString::fromStdString(current_game.category) == cat::cat_disc_game;
- const bool is_in_games_dir = is_disc_game && Emu.IsPathInsideDir(current_game.path, rpcs3::utils::get_games_dir());
- std::vector data_dir_list;
-
- if (is_disc_game)
- {
- data_dir_list = GetDirListBySerial(rpcs3::utils::get_hdd0_dir() + "game/", current_game.serial);
- }
- else
- {
- data_dir_list.push_back(current_game.path);
- }
-
- const bool has_data_dir = !data_dir_list.empty(); // "true" if data path is present (it could be absent for a disc game)
- QString text = tr("%0 - %1\n").arg(QString::fromStdString(current_game.serial)).arg(name);
-
- if (is_disc_game)
- {
- text += tr("\nDisc Game Info:\nPath: %0\n").arg(QString::fromStdString(current_game.path));
-
- if (current_game.size_on_disk != umax) // If size was properly detected
- {
- text += tr("Size: %0\n").arg(gui::utils::format_byte_size(current_game.size_on_disk));
- }
- }
-
- if (has_data_dir)
- {
- u64 total_data_size = 0;
-
- text += tr("\n%0 Info:\n").arg(is_disc_game ? tr("Game Data") : gameinfo->localized_category);
-
- for (const std::string& data_dir : data_dir_list)
- {
- 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 (data_dir_list.size() > 1)
- {
- text += tr("Total size: %0\n").arg(gui::utils::format_byte_size(total_data_size));
- }
- }
-
- if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat)) // Retrieve disk space info on data path's drive
- {
- text += tr("\nCurrent free disk space: %0\n").arg(gui::utils::format_byte_size(stat.avail_free));
- }
-
- if (has_data_dir)
- {
- text += tr("\nPermanently remove %0 and selected (optional) contents from drive?\n").arg(is_disc_game ? tr("Game Data") : gameinfo->localized_category);
- }
- else
- {
- text += tr("\nPermanently remove selected (optional) contents from drive?\n");
- }
-
- QMessageBox mb(QMessageBox::Question, tr("Confirm %0 Removal").arg(gameinfo->localized_category), text, QMessageBox::Yes | QMessageBox::No, this);
- 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 (is_disc_game)
- {
- if (is_in_games_dir)
- {
- disc->setToolTip(tr("Title located under auto-detection \"games\" folder cannot be removed"));
- disc->setDisabled(true);
- }
- else
- {
- disc->setChecked(true);
- }
- }
- else
- {
- disc->setVisible(false);
- }
-
- caches->setChecked(true);
- icons->setChecked(true);
- 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)
- {
- const bool remove_caches = caches->isChecked();
-
- // Remove data path in "dev_hdd0/game" folder (if any)
- if (has_data_dir && RemoveContentPathList(data_dir_list, gameinfo->localized_category.toStdString()) != data_dir_list.size())
- {
- QMessageBox::critical(this, tr("Failure!"), remove_caches
- ? tr("Failed to remove %0 from drive!\nPath: %1\nCaches and custom configs have been left intact.").arg(name).arg(QString::fromStdString(data_dir_list[0]))
- : tr("Failed to remove %0 from drive!\nPath: %1").arg(name).arg(QString::fromStdString(data_dir_list[0])));
-
- return;
- }
-
- // Remove lock file in "dev_hdd0/game/$locks" folder (if any)
- RemoveContentBySerial(rpcs3::utils::get_hdd0_dir() + "game/$locks/", current_game.serial, "lock");
-
- // Remove caches in "cache" and "dev_hdd1/caches" folders (if any) and custom configs in "config/custom_config" folder (if any)
- if (remove_caches)
- {
- RemoveContentPath(cache_base_dir, "cache");
- RemoveHDD1Cache(hdd1_cache_base_dir, current_game.serial);
-
- RemoveCustomConfiguration(current_game.serial);
- RemoveCustomPadConfiguration(current_game.serial);
- }
-
- // Remove icons in "Icons/game_icons" folder, shortcuts in "games/shortcuts" folder and from desktop/start menu
- if (icons->isChecked())
- {
- RemoveContentBySerial(fs::get_config_dir() + "Icons/game_icons/", current_game.serial, "icons");
- RemoveContentBySerial(fs::get_config_dir() + "games/shortcuts/", name.toStdString() + ".lnk", "link");
- // TODO: Remove shortcuts from desktop/start menu
- }
-
- if (savestate->isChecked())
- {
- RemoveContentBySerial(fs::get_config_dir() + "savestates/", current_game.serial, "savestate");
- }
-
- if (captures->isChecked())
- {
- RemoveContentBySerial(fs::get_config_dir() + "captures/", current_game.serial, "captures");
- }
-
- if (recordings->isChecked())
- {
- RemoveContentBySerial(fs::get_config_dir() + "recordings/", current_game.serial, "recordings");
- }
-
- if (screenshots->isChecked())
- {
- RemoveContentBySerial(fs::get_config_dir() + "screenshots/", current_game.serial, "screenshots");
- }
-
- m_game_data.erase(std::remove(m_game_data.begin(), m_game_data.end(), gameinfo), m_game_data.end());
- game_list_log.success("Removed %s - %s", gameinfo->localized_category, current_game.name);
-
- std::vector serials_to_remove_from_yml{};
-
- // Prepare list of serials (title id) to remove in "games.yml" file (if any)
- if (is_disc_game && disc->isChecked())
- {
- serials_to_remove_from_yml.push_back(current_game.serial);
- }
-
- // Finally, refresh the game list.
- // Hidden list in "GuiConfigs/CurrentSettings.ini" file is also properly updated (title removed) if needed
- Refresh(true, serials_to_remove_from_yml);
- }
- });
- 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]
- {
- 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, 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]
- {
- 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]
- {
- 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]
- {
- 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]
- {
- 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);
-}
-
-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& title_id, const game_info& game, bool is_interactive)
-{
- const std::string path = rpcs3::utils::get_custom_config_path(title_id);
-
- if (!fs::is_file(path))
- return true;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove custom game configuration?")) != QMessageBox::Yes)
- 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& title_id, const game_info& game, bool is_interactive)
-{
- if (title_id.empty())
- return true;
-
- const std::string config_dir = rpcs3::utils::get_input_config_dir(title_id);
-
- if (!fs::is_dir(config_dir))
- return true;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), (!Emu.IsStopped(true) && Emu.GetTitleID() == title_id)
- ? tr("Remove custom pad configuration?\nYour configuration will revert to the global pad settings.")
- : tr("Remove custom pad configuration?")) != QMessageBox::Yes)
- return true;
-
- g_cfg_input_configs.load();
- g_cfg_input_configs.active_configs.erase(title_id);
- g_cfg_input_configs.save();
- game_list_log.notice("Removed active input configuration entry for key '%s'", title_id);
-
- if (QDir(QString::fromStdString(config_dir)).removeRecursively())
- {
- if (game)
- {
- game->has_custom_pad_config = false;
- }
- if (!Emu.IsStopped(true) && Emu.GetTitleID() == title_id)
- {
- pad::set_enabled(false);
- pad::reset(title_id);
- pad::set_enabled(true);
- }
- game_list_log.notice("Removed pad configuration directory: %s", config_dir);
- return true;
- }
-
- if (is_interactive)
- {
- QMessageBox::warning(this, tr("Warning!"), tr("Failed to completely remove pad configuration directory!"));
- game_list_log.fatal("Failed to completely remove pad configuration directory: %s\nError: %s", config_dir, fs::g_tls_error);
- }
- return false;
-}
-
-bool game_list_frame::RemoveShadersCache(const std::string& base_dir, bool is_interactive)
-{
- if (!fs::is_dir(base_dir))
- return true;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove shaders cache?")) != QMessageBox::Yes)
- 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 shaders cache dir: %s", filepath);
- }
- else
- {
- game_list_log.warning("Could not completely remove shaders cache dir: %s", filepath);
- }
-
- ++caches_total;
- }
-
- const bool success = caches_total == caches_removed;
-
- if (success)
- game_list_log.success("Removed shaders cache in %s", base_dir);
- else
- game_list_log.fatal("Only %d/%d shaders cache dirs 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& base_dir, bool is_interactive)
-{
- if (!fs::is_dir(base_dir))
- return true;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove PPU cache?")) != QMessageBox::Yes)
- 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& base_dir, bool is_interactive)
-{
- if (!fs::is_dir(base_dir))
- return true;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove SPU cache?")) != QMessageBox::Yes)
- 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;
-}
-
-void game_list_frame::RemoveHDD1Cache(const std::string& base_dir, const std::string& title_id, bool is_interactive)
-{
- if (!fs::is_dir(base_dir))
- return;
-
- if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove HDD1 cache?")) != QMessageBox::Yes)
- return;
-
- u32 dirs_removed = 0;
- u32 dirs_total = 0;
-
- const QString q_base_dir = QString::fromStdString(base_dir);
-
- const QStringList filter{ QString::fromStdString(title_id + "_*") };
-
- 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 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, title_id);
- else
- game_list_log.fatal("Only %d/%d HDD1 cache directories could be removed in %s (%s)", dirs_removed, dirs_total, base_dir, title_id);
-}
-
-void game_list_frame::BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, QString progressLabel, std::function action, std::function cancel_log, 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 (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();
-
- // 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& game_data, bool is_fast_compilation)
-{
- std::set serials;
-
- if (game_data.empty())
- {
- serials.emplace("vsh.self");
- }
-
- for (const auto& game : (game_data.empty() ? m_game_data : game_data))
- {
- 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),
- [&, game_data](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");
- }, false, false,
- []()
- {
- return !Emu.IsStopped(true);
- });
-}
-
-void game_list_frame::BatchRemovePPUCaches()
-{
- if (Emu.GetStatus(false) != system_state::stopped)
- {
- return;
- }
-
- std::set serials;
- serials.emplace("vsh.self");
-
- for (const auto& game : m_game_data)
- {
- 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 caches cleared"),
- [this](const std::string& serial)
- {
- return !serial.empty() &&Emu.IsStopped(true) && RemovePPUCache(GetCacheDirBySerial(serial));
- },
- [this](u32, u32)
- {
- game_list_log.notice("PPU Cache Batch Removal was canceled");
- }, false);
-}
-
-void game_list_frame::BatchRemoveSPUCaches()
-{
- if (Emu.GetStatus(false) != system_state::stopped)
- {
- return;
- }
-
- std::set serials;
- serials.emplace("vsh.self");
-
- for (const auto& game : m_game_data)
- {
- 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 caches cleared"),
- [this](const std::string& serial)
- {
- return !serial.empty() && Emu.IsStopped(true) && RemoveSPUCache(GetCacheDirBySerial(serial));
- },
- [this](u32 removed, u32 total)
- {
- game_list_log.notice("SPU Cache Batch Removal was canceled. %d/%d folders cleared", removed, total);
- }, false);
-}
-
-void game_list_frame::BatchRemoveCustomConfigurations()
-{
- std::set serials;
- for (const auto& game : m_game_data)
- {
- 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);
- }, true);
-}
-
-void game_list_frame::BatchRemoveCustomPadConfigurations()
-{
- std::set serials;
- for (const auto& game : m_game_data)
- {
- 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 Pad Configuration Batch Removal"), tr("No files found"), QMessageBox::Ok);
- return;
- }
-
- progress_dialog* pdlg = new progress_dialog(tr("Custom Pad Configuration Batch Removal"), tr("Removing all custom pad configurations"), tr("Cancel"), 0, total, false, this);
- pdlg->setAutoClose(false);
- pdlg->setAutoReset(false);
- pdlg->open();
-
- BatchActionBySerials(pdlg, serials, tr("%0/%1 custom pad 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 Pad Configuration Batch Removal was canceled. %d/%d custom pad configurations cleared", removed, total);
- }, true);
-}
-
-void game_list_frame::BatchRemoveShaderCaches()
-{
- if (Emu.GetStatus(false) != system_state::stopped)
- {
- return;
- }
-
- std::set serials;
- serials.emplace("vsh.self");
-
- for (const auto& game : m_game_data)
- {
- 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) && RemoveShadersCache(GetCacheDirBySerial(serial));
- },
- [this](u32 removed, u32 total)
- {
- game_list_log.notice("Shader Cache Batch Removal was canceled. %d/%d cleared", removed, total);
- }, false);
}
void game_list_frame::ShowCustomConfigIcon(const game_info& game)
@@ -3001,38 +1257,37 @@ bool game_list_frame::SearchMatchesApp(const QString& name, const QString& seria
return true;
}
-std::string game_list_frame::CurrentSelectionPath()
+std::set game_list_frame::CurrentSelectionPaths()
{
- std::string selection;
-
- game_info game{};
+ std::set selection;
if (m_old_layout_is_list)
{
- if (!m_game_list->selectedItems().isEmpty())
+ for (const QTableWidgetItem* selected_item : m_game_list->selectedItems())
{
- if (QTableWidgetItem* item = m_game_list->item(m_game_list->currentRow(), 0))
+ if (const QTableWidgetItem* item = m_game_list->item(selected_item->row(), 0))
{
if (const QVariant var = item->data(gui::game_role); var.canConvert())
{
- game = var.value();
+ if (const game_info game = var.value())
+ {
+ selection.insert(game->info.path + game->info.icon_path);
+ }
}
}
}
}
else if (m_game_grid)
{
- if (game_list_grid_item* item = static_cast(m_game_grid->selected_item()))
+ if (const game_list_grid_item* item = static_cast(m_game_grid->selected_item()))
{
- game = item->game();
+ if (const game_info& game = item->game())
+ {
+ selection.insert(game->info.path + game->info.icon_path);
+ }
}
}
- if (game)
- {
- selection = game->info.path + game->info.icon_path;
- }
-
m_old_layout_is_list = m_is_list_layout;
return selection;
@@ -3106,11 +1361,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 bcdd643ca2..b8148630f9 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,20 +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;
+ void ShowCustomConfigIcon(const game_info& game);
+
+ // 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);
+ }
+
public Q_SLOTS:
- void BatchCreateCPUCaches(const std::vector& game_data = {}, bool is_fast_compilation = false);
- void BatchRemovePPUCaches();
- void BatchRemoveSPUCaches();
- void BatchRemoveCustomConfigurations();
- void BatchRemoveCustomPadConfigurations();
- void BatchRemoveShaderCaches();
void SetListMode(bool is_list);
void SetSearchText(const QString& text);
void SetShowCompatibilityInGrid(bool show);
@@ -87,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);
@@ -97,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
{
@@ -105,50 +123,14 @@ 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;
- bool RemoveCustomConfiguration(const std::string& title_id, const game_info& game = nullptr, bool is_interactive = false);
- bool RemoveCustomPadConfiguration(const std::string& title_id, const game_info& game = nullptr, bool is_interactive = false);
- bool RemoveShadersCache(const std::string& base_dir, bool is_interactive = false);
- bool RemovePPUCache(const std::string& base_dir, bool is_interactive = false);
- bool RemoveSPUCache(const std::string& base_dir, bool is_interactive = false);
- void RemoveHDD1Cache(const std::string& base_dir, const std::string& title_id, 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);
-
- static bool RemoveContentPath(const std::string& path, const std::string& desc);
- static u32 RemoveContentPathList(const std::vector& path_list, const std::string& desc);
- static bool RemoveContentBySerial(const std::string& base_dir, const std::string& serial, const std::string& desc);
- static std::vector GetDirListBySerial(const std::string& base_dir, const std::string& serial);
- void BatchActionBySerials(progress_dialog* pdlg, const std::set& serials, QString progressLabel, std::function action, std::function cancel_log, bool refresh_on_finish, bool can_be_concurrent = false, std::function should_wait_cb = {});
- static std::string GetCacheDirBySerial(const std::string& serial);
- static std::string GetDataDirBySerial(const std::string& serial);
- std::string CurrentSelectionPath();
+ std::set CurrentSelectionPaths();
game_info GetGameInfoByMode(const QTableWidgetItem* item) const;
static game_info GetGameInfoFromItem(const QTableWidgetItem* item);
@@ -156,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..e60a832569 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()
@@ -42,7 +40,7 @@ void game_list_grid::populate(
const std::vector& game_data,
const std::map& notes_map,
const std::map& title_map,
- const std::string& selected_item_id,
+ const std::set& selected_item_ids,
bool play_hover_movies)
{
clear_list();
@@ -112,7 +110,7 @@ void game_list_grid::populate(
item->set_video_path(game->info.movie_path);
}
- if (selected_item_id == game->info.path + game->info.icon_path)
+ if (selected_item_ids.contains(game->info.path + game->info.icon_path))
{
selected_item = item;
}
diff --git a/rpcs3/rpcs3qt/game_list_grid.h b/rpcs3/rpcs3qt/game_list_grid.h
index 6116ff1d8c..e9e5890e81 100644
--- a/rpcs3/rpcs3qt/game_list_grid.h
+++ b/rpcs3/rpcs3qt/game_list_grid.h
@@ -18,7 +18,7 @@ public:
const std::vector& game_data,
const std::map& notes_map,
const std::map& title_map,
- const std::string& selected_item_id,
+ const std::set& selected_item_ids,
bool play_hover_movies) override;
void repaint_icons(std::vector& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio) override;
diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp
index 8d721f86ec..c3069ef67e 100644
--- a/rpcs3/rpcs3qt/game_list_table.cpp
+++ b/rpcs3/rpcs3qt/game_list_table.cpp
@@ -24,7 +24,7 @@ game_list_table::game_list_table(game_list_frame* frame, std::shared_ptrsetSingleStep(20);
@@ -204,7 +204,7 @@ void game_list_table::populate(
const std::vector& game_data,
const std::map& notes_map,
const std::map& title_map,
- const std::string& selected_item_id,
+ const std::set& selected_item_ids,
bool play_hover_movies)
{
clear_list();
@@ -219,7 +219,7 @@ void game_list_table::populate(
int row = 0;
int index = -1;
- int selected_row = -1;
+ std::set selected_rows;
const auto get_title = [&title_map](const QString& serial, const std::string& name) -> QString
{
@@ -378,15 +378,18 @@ void game_list_table::populate(
setItem(row, static_cast(gui::game_list_columns::compat), compat_item);
setItem(row, static_cast(gui::game_list_columns::dir_size), new custom_table_widget_item(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"), Qt::UserRole, QVariant::fromValue(game_size)));
- if (selected_item_id == game->info.path + game->info.icon_path)
+ if (selected_item_ids.contains(game->info.path + game->info.icon_path))
{
- selected_row = row;
+ selected_rows.insert(row);
}
row++;
}
- selectRow(selected_row);
+ for (int selected_row : selected_rows)
+ {
+ selectionModel()->select(model()->index(selected_row, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
}
void game_list_table::repaint_icons(std::vector& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio)
diff --git a/rpcs3/rpcs3qt/game_list_table.h b/rpcs3/rpcs3qt/game_list_table.h
index 4c185fcc61..ac9bff64e5 100644
--- a/rpcs3/rpcs3qt/game_list_table.h
+++ b/rpcs3/rpcs3qt/game_list_table.h
@@ -28,7 +28,7 @@ public:
const std::vector& game_data,
const std::map& notes_map,
const std::map& title_map,
- const std::string& selected_item_id,
+ const std::set& selected_item_ids,
bool play_hover_movies) override;
void repaint_icons(std::vector& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio) override;
diff --git a/rpcs3/rpcs3qt/log_viewer.cpp b/rpcs3/rpcs3qt/log_viewer.cpp
index 99961809c9..3a566937a7 100644
--- a/rpcs3/rpcs3qt/log_viewer.cpp
+++ b/rpcs3/rpcs3qt/log_viewer.cpp
@@ -446,6 +446,7 @@ void log_viewer::dropEvent(QDropEvent* ev)
{
if (is_valid_file(*ev->mimeData(), true))
{
+ ev->acceptProposedAction();
show_log();
}
}
@@ -454,7 +455,7 @@ void log_viewer::dragEnterEvent(QDragEnterEvent* ev)
{
if (is_valid_file(*ev->mimeData()))
{
- ev->accept();
+ ev->acceptProposedAction();
}
}
@@ -462,15 +463,10 @@ void log_viewer::dragMoveEvent(QDragMoveEvent* ev)
{
if (is_valid_file(*ev->mimeData()))
{
- ev->accept();
+ ev->acceptProposedAction();
}
}
-void log_viewer::dragLeaveEvent(QDragLeaveEvent* ev)
-{
- ev->accept();
-}
-
bool log_viewer::eventFilter(QObject* object, QEvent* event)
{
if (object != m_log_text)
diff --git a/rpcs3/rpcs3qt/log_viewer.h b/rpcs3/rpcs3qt/log_viewer.h
index 320b2b97c9..85ece2688b 100644
--- a/rpcs3/rpcs3qt/log_viewer.h
+++ b/rpcs3/rpcs3qt/log_viewer.h
@@ -43,6 +43,5 @@ protected:
void dropEvent(QDropEvent* ev) override;
void dragEnterEvent(QDragEnterEvent* ev) override;
void dragMoveEvent(QDragMoveEvent* ev) override;
- void dragLeaveEvent(QDragLeaveEvent* ev) override;
bool eventFilter(QObject* object, QEvent* event) override;
};
diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp
index fea8dc8ca4..485f7e3f22 100644
--- a/rpcs3/rpcs3qt/main_window.cpp
+++ b/rpcs3/rpcs3qt/main_window.cpp
@@ -221,7 +221,7 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot)
if (enable_play_last)
{
- ui->sysPauseAct->setText(tr("&Play last played game"));
+ ui->sysPauseAct->setText(tr("&Play Last Played Game"));
ui->sysPauseAct->setIcon(m_icon_play);
ui->toolbar_start->setToolTip(start_tooltip);
}
@@ -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);
}
}
@@ -2722,15 +2722,43 @@ void main_window::CreateConnects()
});
connect(ui->exitAct, &QAction::triggered, this, &QWidget::close);
- connect(ui->batchCreateCPUCachesAct, &QAction::triggered, m_game_list_frame, [list = m_game_list_frame]() { list->BatchCreateCPUCaches(); });
- connect(ui->batchRemoveCustomConfigurationsAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveCustomConfigurations);
- connect(ui->batchRemoveCustomPadConfigurationsAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveCustomPadConfigurations);
- connect(ui->batchRemoveShaderCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveShaderCaches);
- connect(ui->batchRemovePPUCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemovePPUCaches);
- connect(ui->batchRemoveSPUCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveSPUCaches);
- connect(ui->removeHDD1CachesAct, &QAction::triggered, this, &main_window::RemoveHDD1Caches);
- connect(ui->removeAllCachesAct, &QAction::triggered, this, &main_window::RemoveAllCaches);
- connect(ui->removeSavestatesAct, &QAction::triggered, this, &main_window::RemoveSavestates);
+ connect(ui->batchCreateCPUCachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchCreateCPUCaches({}, false, true);
+ });
+ connect(ui->batchRemoveCustomConfigurationsAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveCustomConfigurations({}, true);
+ });
+ connect(ui->batchRemoveCustomPadConfigurationsAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveCustomPadConfigurations({}, true);
+ });
+ connect(ui->batchRemoveShaderCachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveShaderCaches({}, true);
+ });
+ connect(ui->batchRemovePPUCachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemovePPUCaches({}, true);
+ });
+ connect(ui->batchRemoveSPUCachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveSPUCaches({}, true);
+ });
+ connect(ui->removeHDD1CachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveHDD1Caches({}, true);
+ });
+ connect(ui->removeAllCachesAct, &QAction::triggered, this, [this]()
+ {
+ m_game_list_frame->actions()->BatchRemoveAllCaches({}, true);
+ });
+ connect(ui->removeSavestatesAct, &QAction::triggered, this, [this]()
+ {
+ 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);
connect(ui->removeFirmwareCacheAct, &QAction::triggered, this, &main_window::RemoveFirmwareCache);
@@ -3659,67 +3687,6 @@ void main_window::SetIconSizeActions(int idx) const
ui->setIconSizeLargeAct->setChecked(true);
}
-void main_window::RemoveHDD1Caches()
-{
- if (fs::remove_all(rpcs3::utils::get_hdd1_dir() + "caches", false))
- {
- QMessageBox::information(this, tr("HDD1 Caches Removed"), tr("HDD1 caches successfully removed"));
- }
- else
- {
- QMessageBox::warning(this, tr("Error"), tr("Could not remove HDD1 caches"));
- }
-}
-
-void main_window::RemoveAllCaches()
-{
- if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove all caches?")) != QMessageBox::Yes)
- return;
-
- const std::string cache_base_dir = rpcs3::utils::get_cache_dir();
- u64 caches_count = 0;
- u64 caches_removed = 0;
-
- for (const game_info& game : m_game_list_frame->GetGameInfo()) // Loop on detected games
- {
- if (game && QString::fromStdString(game->info.category) != cat::cat_ps3_os && fs::exists(cache_base_dir + game->info.serial)) // If not OS category and cache exists
- {
- caches_count++;
-
- if (fs::remove_all(cache_base_dir + game->info.serial))
- {
- caches_removed++;
- }
- }
- }
-
- if (caches_count == caches_removed)
- {
- QMessageBox::information(this, tr("Caches Removed"), tr("%0 cache(s) successfully removed").arg(caches_removed));
- }
- else
- {
- QMessageBox::warning(this, tr("Error"), tr("Could not remove %0 of %1 cache(s)").arg(caches_count - caches_removed).arg(caches_count));
- }
-
- RemoveHDD1Caches();
-}
-
-void main_window::RemoveSavestates()
-{
- if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove savestates?")) != QMessageBox::Yes)
- return;
-
- if (fs::remove_all(fs::get_config_dir() + "savestates", false))
- {
- QMessageBox::information(this, tr("Savestates Removed"), tr("Savestates successfully removed"));
- }
- else
- {
- QMessageBox::warning(this, tr("Error"), tr("Could not remove savestates"));
- }
-}
-
void main_window::CleanUpGameList()
{
if (QMessageBox::question(this, tr("Confirm Removal"), tr("Remove invalid game paths from game list?\n"
@@ -4077,15 +4044,18 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList
void main_window::dropEvent(QDropEvent* event)
{
- event->accept();
-
QStringList drop_paths;
+ const drop_type type = IsValidFile(*event->mimeData(), &drop_paths);
- switch (IsValidFile(*event->mimeData(), &drop_paths)) // get valid file paths and drop type
+ if (type != drop_type::drop_error)
+ {
+ event->acceptProposedAction();
+ }
+
+ switch (type) // get valid file paths and drop type
{
case drop_type::drop_error:
{
- event->ignore();
break;
}
case drop_type::drop_rap_edat_pkg: // install the packages
@@ -4167,15 +4137,16 @@ void main_window::dropEvent(QDropEvent* event)
void main_window::dragEnterEvent(QDragEnterEvent* event)
{
- event->setAccepted(IsValidFile(*event->mimeData()) != drop_type::drop_error);
+ if (IsValidFile(*event->mimeData()) != drop_type::drop_error)
+ {
+ event->acceptProposedAction();
+ }
}
void main_window::dragMoveEvent(QDragMoveEvent* event)
{
- event->setAccepted(IsValidFile(*event->mimeData()) != drop_type::drop_error);
-}
-
-void main_window::dragLeaveEvent(QDragLeaveEvent* event)
-{
- event->accept();
+ if (IsValidFile(*event->mimeData()) != drop_type::drop_error)
+ {
+ event->acceptProposedAction();
+ }
}
diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h
index 8a82c125ce..ada8f01cad 100644
--- a/rpcs3/rpcs3qt/main_window.h
+++ b/rpcs3/rpcs3qt/main_window.h
@@ -114,9 +114,6 @@ private Q_SLOTS:
void SetIconSizeActions(int idx) const;
void ResizeIcons(int index);
- void RemoveHDD1Caches();
- void RemoveAllCaches();
- void RemoveSavestates();
void CleanUpGameList();
void RemoveFirmwareCache();
@@ -131,7 +128,6 @@ protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
- void dragLeaveEvent(QDragLeaveEvent* event) override;
private:
void ConfigureGuiFromSettings();
diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui
index 39a3c014bb..bf75029d38 100644
--- a/rpcs3/rpcs3qt/main_window.ui
+++ b/rpcs3/rpcs3qt/main_window.ui
@@ -1174,7 +1174,7 @@
- Remove Custom Pad Configurations
+ Remove Custom Gamepad Configurations
diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.cpp b/rpcs3/rpcs3qt/patch_manager_dialog.cpp
index 04c8906e9b..227a4a3a47 100644
--- a/rpcs3/rpcs3qt/patch_manager_dialog.cpp
+++ b/rpcs3/rpcs3qt/patch_manager_dialog.cpp
@@ -1020,6 +1020,8 @@ void patch_manager_dialog::dropEvent(QDropEvent* event)
return;
}
+ event->acceptProposedAction();
+
QMessageBox box(QMessageBox::Icon::Question, tr("Patch Manager"), tr("What do you want to do with the patch file?"), QMessageBox::StandardButton::Cancel, this);
QPushButton* button_yes = box.addButton(tr("Import"), QMessageBox::YesRole);
QPushButton* button_no = box.addButton(tr("Validate"), QMessageBox::NoRole);
@@ -1123,7 +1125,7 @@ void patch_manager_dialog::dragEnterEvent(QDragEnterEvent* event)
{
if (is_valid_file(*event->mimeData()))
{
- event->accept();
+ event->acceptProposedAction();
}
}
@@ -1131,15 +1133,10 @@ void patch_manager_dialog::dragMoveEvent(QDragMoveEvent* event)
{
if (is_valid_file(*event->mimeData()))
{
- event->accept();
+ event->acceptProposedAction();
}
}
-void patch_manager_dialog::dragLeaveEvent(QDragLeaveEvent* event)
-{
- event->accept();
-}
-
void patch_manager_dialog::download_update(bool automatic, bool auto_accept)
{
patch_log.notice("Patch download triggered (automatic=%d, auto_accept=%d)", automatic, auto_accept);
diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.h b/rpcs3/rpcs3qt/patch_manager_dialog.h
index e65bcfb7d3..a8c70c2caa 100644
--- a/rpcs3/rpcs3qt/patch_manager_dialog.h
+++ b/rpcs3/rpcs3qt/patch_manager_dialog.h
@@ -83,6 +83,5 @@ protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
- void dragLeaveEvent(QDragLeaveEvent* event) override;
void closeEvent(QCloseEvent* event) override;
};