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