From bd1ebb7a10d66d631c1a1bc21defed6ecbb74cba Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 23 Jan 2025 22:06:54 +0100 Subject: [PATCH] Qt: add savestate manager dialog --- rpcs3/rpcs3.vcxproj | 17 + rpcs3/rpcs3.vcxproj.filters | 15 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/game_list_frame.cpp | 4 +- rpcs3/rpcs3qt/gui_settings.cpp | 173 +++-- rpcs3/rpcs3qt/gui_settings.h | 35 + rpcs3/rpcs3qt/main_window.cpp | 9 + rpcs3/rpcs3qt/main_window.ui | 22 +- rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 794 +++++++++++++++++++++ rpcs3/rpcs3qt/savestate_manager_dialog.h | 85 +++ rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 3 +- 11 files changed, 1074 insertions(+), 84 deletions(-) create mode 100644 rpcs3/rpcs3qt/savestate_manager_dialog.cpp create mode 100644 rpcs3/rpcs3qt/savestate_manager_dialog.h diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index adb7e376be..3e703c9af0 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -439,6 +439,9 @@ true + + true + true @@ -712,6 +715,9 @@ true + + true + true @@ -827,6 +833,7 @@ + @@ -1819,6 +1826,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-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\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + + $(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 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-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\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(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 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-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\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) Moc%27ing %(Identity)... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 4cdbcedc2e..199495aaba 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -193,6 +193,9 @@ {f6b701aa-7f4a-4816-b05f-80d24cb70e13} + + {9b51636c-b371-425b-86d3-be003774a1b7} + @@ -471,6 +474,9 @@ Gui\trophy + + Gui\savestates + Gui\misc dialogs @@ -549,6 +555,12 @@ Generated Files\Release + + Generated Files\Debug + + + Generated Files\Release + Gui\misc dialogs @@ -1480,6 +1492,9 @@ Gui\trophy + + Gui\savestates + Gui\misc dialogs diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index e3e3c0a264..140b39644d 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -80,6 +80,7 @@ add_library(rpcs3_ui STATIC save_data_info_dialog.cpp save_data_list_dialog.cpp save_manager_dialog.cpp + savestate_manager_dialog.cpp screenshot_item.cpp screenshot_manager_dialog.cpp screenshot_preview.cpp diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 1699837ae3..150724deea 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -1197,9 +1197,9 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) }); } - extern bool is_savestate_compatible(fs::file&& file, std::string_view filepath); + extern bool is_savestate_compatible(const std::string& filepath); - if (const std::string sstate = get_savestate_file(current_game.serial, current_game.path, 0, 0); is_savestate_compatible(fs::file(sstate), sstate)) + if (const std::string sstate = get_savestate_file(current_game.serial, current_game.path, 0, 0); is_savestate_compatible(sstate)) { QAction* boot_state = menu.addAction(is_current_running_game ? tr("&Reboot with savestate") diff --git a/rpcs3/rpcs3qt/gui_settings.cpp b/rpcs3/rpcs3qt/gui_settings.cpp index a79f9d9db7..cb258fb5f0 100644 --- a/rpcs3/rpcs3qt/gui_settings.cpp +++ b/rpcs3/rpcs3qt/gui_settings.cpp @@ -20,42 +20,53 @@ namespace gui QString stylesheet; bool custom_stylesheet_active = false; + QString get_savestate_list_column_name(savestate_list_columns col) + { + switch (col) + { + case gui::savestate_list_columns::name: return "savestate_column_name"; + case gui::savestate_list_columns::compatible: return "savestate_column_compatible"; + case gui::savestate_list_columns::date: return "savestate_column_date"; + case gui::savestate_list_columns::path: return "savestate_column_path"; + case gui::savestate_list_columns::count: return ""; + } + + fmt::throw_exception("get_savestate_list_column_name: Invalid column"); + } + + QString get_savestate_game_list_column_name(savestate_game_list_columns col) + { + switch (col) + { + case gui::savestate_game_list_columns::icon: return "savestate_game_column_icon"; + case gui::savestate_game_list_columns::name: return "savestate_game_column_name"; + case gui::savestate_game_list_columns::savestates: return "savestate_game_column_savestates"; + case gui::savestate_game_list_columns::count: return ""; + } + + fmt::throw_exception("get_savestate_game_list_column_name: Invalid column"); + } + QString get_game_list_column_name(game_list_columns col) { switch (col) { - case game_list_columns::icon: - return "column_icon"; - case game_list_columns::name: - return "column_name"; - case game_list_columns::serial: - return "column_serial"; - case game_list_columns::firmware: - return "column_firmware"; - case game_list_columns::version: - return "column_version"; - case game_list_columns::category: - return "column_category"; - case game_list_columns::path: - return "column_path"; - case game_list_columns::move: - return "column_move"; - case game_list_columns::resolution: - return "column_resolution"; - case game_list_columns::sound: - return "column_sound"; - case game_list_columns::parental: - return "column_parental"; - case game_list_columns::last_play: - return "column_last_play"; - case game_list_columns::playtime: - return "column_playtime"; - case game_list_columns::compat: - return "column_compat"; - case game_list_columns::dir_size: - return "column_dir_size"; - case game_list_columns::count: - return ""; + case game_list_columns::icon: return "column_icon"; + case game_list_columns::name: return "column_name"; + case game_list_columns::serial: return "column_serial"; + case game_list_columns::firmware: return "column_firmware"; + case game_list_columns::version: return "column_version"; + case game_list_columns::category: return "column_category"; + case game_list_columns::path: return "column_path"; + case game_list_columns::move: return "column_move"; + case game_list_columns::resolution: return "column_resolution"; + case game_list_columns::sound: return "column_sound"; + case game_list_columns::parental: return "column_parental"; + case game_list_columns::last_play: return "column_last_play"; + case game_list_columns::playtime: return "column_playtime"; + case game_list_columns::compat: return "column_compat"; + case game_list_columns::dir_size: return "column_dir_size"; + case game_list_columns::count: return ""; } fmt::throw_exception("get_game_list_column_name: Invalid column"); @@ -65,24 +76,15 @@ namespace gui { switch (col) { - case trophy_list_columns::icon: - return "trophy_column_icon"; - case trophy_list_columns::name: - return "trophy_column_name"; - case trophy_list_columns::description: - return "trophy_column_description"; - case trophy_list_columns::type: - return "trophy_column_type"; - case trophy_list_columns::is_unlocked: - return "trophy_column_is_unlocked"; - case trophy_list_columns::id: - return "trophy_column_id"; - case trophy_list_columns::platinum_link: - return "trophy_column_platinum_link"; - case trophy_list_columns::time_unlocked: - return "trophy_column_time_unlocked"; - case trophy_list_columns::count: - return ""; + case trophy_list_columns::icon: return "trophy_column_icon"; + case trophy_list_columns::name: return "trophy_column_name"; + case trophy_list_columns::description: return "trophy_column_description"; + case trophy_list_columns::type: return "trophy_column_type"; + case trophy_list_columns::is_unlocked: return "trophy_column_is_unlocked"; + case trophy_list_columns::id: return "trophy_column_id"; + case trophy_list_columns::platinum_link: return "trophy_column_platinum_link"; + case trophy_list_columns::time_unlocked: return "trophy_column_time_unlocked"; + case trophy_list_columns::count: return ""; } fmt::throw_exception("get_trophy_list_column_name: Invalid column"); @@ -92,16 +94,11 @@ namespace gui { switch (col) { - case trophy_game_list_columns::icon: - return "trophy_game_column_icon"; - case trophy_game_list_columns::name: - return "trophy_game_column_name"; - case trophy_game_list_columns::progress: - return "trophy_game_column_progress"; - case trophy_game_list_columns::trophies: - return "trophy_game_column_trophies"; - case trophy_game_list_columns::count: - return ""; + case trophy_game_list_columns::icon: return "trophy_game_column_icon"; + case trophy_game_list_columns::name: return "trophy_game_column_name"; + case trophy_game_list_columns::progress: return "trophy_game_column_progress"; + case trophy_game_list_columns::trophies: return "trophy_game_column_trophies"; + case trophy_game_list_columns::count: return ""; } fmt::throw_exception("get_trophy_game_list_column_name: Invalid column"); @@ -115,21 +112,21 @@ gui_settings::gui_settings(QObject* parent) : settings(parent) QStringList gui_settings::GetGameListCategoryFilters(bool is_list_mode) const { - QStringList filterList; + QStringList filters; - if (GetCategoryVisibility(Category::HDD_Game, is_list_mode)) filterList.append(cat::cat_hdd_game); - if (GetCategoryVisibility(Category::Disc_Game, is_list_mode)) filterList.append(cat::cat_disc_game); - if (GetCategoryVisibility(Category::PS1_Game, is_list_mode)) filterList.append(cat::cat_ps1_game); - if (GetCategoryVisibility(Category::PS2_Game, is_list_mode)) filterList.append(cat::ps2_games); - if (GetCategoryVisibility(Category::PSP_Game, is_list_mode)) filterList.append(cat::psp_games); - if (GetCategoryVisibility(Category::Home, is_list_mode)) filterList.append(cat::cat_home); - if (GetCategoryVisibility(Category::Media, is_list_mode)) filterList.append(cat::media); - if (GetCategoryVisibility(Category::Data, is_list_mode)) filterList.append(cat::data); - if (GetCategoryVisibility(Category::OS, is_list_mode)) filterList.append(cat::os); - if (GetCategoryVisibility(Category::Unknown_Cat, is_list_mode)) filterList.append(cat::cat_unknown); - if (GetCategoryVisibility(Category::Others, is_list_mode)) filterList.append(cat::others); + if (GetCategoryVisibility(Category::HDD_Game, is_list_mode)) filters.append(cat::cat_hdd_game); + if (GetCategoryVisibility(Category::Disc_Game, is_list_mode)) filters.append(cat::cat_disc_game); + if (GetCategoryVisibility(Category::PS1_Game, is_list_mode)) filters.append(cat::cat_ps1_game); + if (GetCategoryVisibility(Category::PS2_Game, is_list_mode)) filters.append(cat::ps2_games); + if (GetCategoryVisibility(Category::PSP_Game, is_list_mode)) filters.append(cat::psp_games); + if (GetCategoryVisibility(Category::Home, is_list_mode)) filters.append(cat::cat_home); + if (GetCategoryVisibility(Category::Media, is_list_mode)) filters.append(cat::media); + if (GetCategoryVisibility(Category::Data, is_list_mode)) filters.append(cat::data); + if (GetCategoryVisibility(Category::OS, is_list_mode)) filters.append(cat::os); + if (GetCategoryVisibility(Category::Unknown_Cat, is_list_mode)) filters.append(cat::cat_unknown); + if (GetCategoryVisibility(Category::Others, is_list_mode)) filters.append(cat::others); - return filterList; + return filters; } bool gui_settings::GetCategoryVisibility(int cat, bool is_list_mode) const @@ -243,6 +240,16 @@ bool gui_settings::GetBootConfirmation(QWidget* parent, const gui_save& gui_save return true; } +void gui_settings::SetSavestateGamelistColVisibility(gui::savestate_game_list_columns col, bool val) const +{ + SetValue(GetGuiSaveForSavestateGameColumn(col), val); +} + +void gui_settings::SetSavestateListColVisibility(gui::savestate_list_columns col, bool val) const +{ + SetValue(GetGuiSaveForSavestateColumn(col), val); +} + void gui_settings::SetTrophyGamelistColVisibility(gui::trophy_game_list_columns col, bool val) const { SetValue(GetGuiSaveForTrophyGameColumn(col), val); @@ -268,6 +275,16 @@ logs::level gui_settings::GetLogLevel() const return logs::level(GetValue(gui::l_level).toUInt()); } +bool gui_settings::GetSavestateGamelistColVisibility(gui::savestate_game_list_columns col) const +{ + return GetValue(GetGuiSaveForSavestateGameColumn(col)).toBool(); +} + +bool gui_settings::GetSavestateListColVisibility(gui::savestate_list_columns col) const +{ + return GetValue(GetGuiSaveForSavestateColumn(col)).toBool(); +} + bool gui_settings::GetTrophyGamelistColVisibility(gui::trophy_game_list_columns col) const { return GetValue(GetGuiSaveForTrophyGameColumn(col)).toBool(); @@ -316,6 +333,16 @@ QSize gui_settings::SizeFromSlider(int pos) return gui::gl_icon_size_min + (gui::gl_icon_size_max - gui::gl_icon_size_min) * (1.f * pos / gui::gl_max_slider_pos); } +gui_save gui_settings::GetGuiSaveForSavestateGameColumn(gui::savestate_game_list_columns col) +{ + return gui_save{ gui::savestate, "visibility_" + gui::get_savestate_game_list_column_name(col), true }; +} + +gui_save gui_settings::GetGuiSaveForSavestateColumn(gui::savestate_list_columns col) +{ + return gui_save{ gui::savestate, "visibility_" + gui::get_savestate_list_column_name(col), true }; +} + gui_save gui_settings::GetGuiSaveForTrophyGameColumn(gui::trophy_game_list_columns col) { return gui_save{ gui::trophy, "visibility_" + gui::get_trophy_game_list_column_name(col), true }; diff --git a/rpcs3/rpcs3qt/gui_settings.h b/rpcs3/rpcs3qt/gui_settings.h index 81cfe6bcc1..b3fab563cf 100644 --- a/rpcs3/rpcs3qt/gui_settings.h +++ b/rpcs3/rpcs3qt/gui_settings.h @@ -64,6 +64,27 @@ namespace gui count }; + enum class savestate_game_list_columns + { + icon = 0, + name = 1, + savestates = 2, + + count + }; + + enum class savestate_list_columns + { + name = 0, + compatible = 1, + date = 2, + path = 3, + + count + }; + + QString get_savestate_game_list_column_name(savestate_game_list_columns col); + QString get_savestate_list_column_name(savestate_list_columns col); QString get_trophy_game_list_column_name(trophy_game_list_columns col); QString get_trophy_list_column_name(trophy_list_columns col); QString get_game_list_column_name(game_list_columns col); @@ -109,6 +130,7 @@ namespace gui const QString log_viewer = "LogViewer"; const QString sc = "Shortcuts"; const QString navigation = "PadNavigation"; + const QString savestate = "Savestate"; const QString update_on = "true"; const QString update_off = "false"; @@ -246,6 +268,13 @@ namespace gui const gui_save gs_geometry = gui_save(gs_frame, "geometry", QRect()); const gui_save gs_visibility = gui_save(gs_frame, "visibility", QWindow::Visibility::AutomaticVisibility); + const gui_save ss_icon_color = gui_save(trophy, "icon_color", gl_icon_color); + const gui_save ss_game_icon_size = gui_save(trophy, "game_icon_size", 25); + const gui_save ss_geometry = gui_save(trophy, "geometry", QByteArray()); + const gui_save ss_splitterState = gui_save(trophy, "splitterState", QByteArray()); + const gui_save ss_games_state = gui_save(trophy, "games_state", QByteArray()); + const gui_save ss_savestate_state = gui_save(trophy, "savestate_state", QByteArray()); + const gui_save tr_icon_color = gui_save(trophy, "icon_color", gl_icon_color); const gui_save tr_icon_height = gui_save(trophy, "icon_height", 75); const gui_save tr_game_iconSize = gui_save(trophy, "game_iconSize", 25); @@ -304,6 +333,8 @@ public: bool GetBootConfirmation(QWidget* parent, const gui_save& gui_save_entry = gui_save()); logs::level GetLogLevel() const; + bool GetSavestateGamelistColVisibility(gui::savestate_game_list_columns col) const; + bool GetSavestateListColVisibility(gui::savestate_list_columns col) const; bool GetTrophyGamelistColVisibility(gui::trophy_game_list_columns col) const; bool GetTrophylistColVisibility(gui::trophy_list_columns col) const; bool GetGamelistColVisibility(gui::game_list_columns col) const; @@ -316,6 +347,8 @@ public: /** Sets the visibility of the chosen category. */ void SetCategoryVisibility(int cat, bool val, bool is_list_mode) const; + void SetSavestateGamelistColVisibility(gui::savestate_game_list_columns col, bool val) const; + void SetSavestateListColVisibility(gui::savestate_list_columns col, bool val) const; void SetTrophyGamelistColVisibility(gui::trophy_game_list_columns col, bool val) const; void SetTrophylistColVisibility(gui::trophy_list_columns col, bool val) const; void SetGamelistColVisibility(gui::game_list_columns col, bool val) const; @@ -323,6 +356,8 @@ public: void SetCustomColor(int col, const QColor& val) const; private: + static gui_save GetGuiSaveForSavestateGameColumn(gui::savestate_game_list_columns col); + static gui_save GetGuiSaveForSavestateColumn(gui::savestate_list_columns col); static gui_save GetGuiSaveForTrophyGameColumn(gui::trophy_game_list_columns col); static gui_save GetGuiSaveForTrophyColumn(gui::trophy_list_columns col); static gui_save GetGuiSaveForGameColumn(gui::game_list_columns col); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index e4cccd3307..3e521f0729 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -3,6 +3,7 @@ #include "vfs_dialog.h" #include "save_manager_dialog.h" #include "trophy_manager_dialog.h" +#include "savestate_manager_dialog.h" #include "user_manager_dialog.h" #include "screenshot_manager_dialog.h" #include "kernel_explorer.h" @@ -3054,6 +3055,14 @@ void main_window::CreateConnects() trop_manager->show(); }); + connect(ui->actionManage_Savestates, &QAction::triggered, this, [this] + { + savestate_manager_dialog* manager = new savestate_manager_dialog(m_gui_settings, m_game_list_frame->GetGameInfo()); + connect(this, &main_window::RequestDialogRepaint, manager, &savestate_manager_dialog::HandleRepaintUiRequest); + connect(manager, &savestate_manager_dialog::RequestBoot, this, [this](const std::string& path) { Boot(path, "", true); }); + manager->show(); + }); + connect(ui->actionManage_Skylanders_Portal, &QAction::triggered, this, [this] { skylander_dialog* sky_diag = skylander_dialog::get_dlg(this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 3ddf7b8b1a..b9f28cdfff 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -67,7 +67,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus false @@ -119,16 +119,16 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus false - Qt::Horizontal + Qt::Orientation::Horizontal - QSlider::NoTicks + QSlider::TickPosition::NoTicks @@ -145,7 +145,7 @@ - Qt::PreventContextMenu + Qt::ContextMenuPolicy::PreventContextMenu @@ -297,6 +297,7 @@ + @@ -422,7 +423,7 @@ - Qt::PreventContextMenu + Qt::ContextMenuPolicy::PreventContextMenu Show tool bar @@ -431,7 +432,7 @@ false - Qt::TopToolBarArea + Qt::ToolBarArea::TopToolBarArea @@ -440,7 +441,7 @@ - Qt::ToolButtonTextUnderIcon + Qt::ToolButtonStyle::ToolButtonTextUnderIcon false @@ -1406,6 +1407,11 @@ List Freeze + + + Savestates + + diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp new file mode 100644 index 0000000000..463931b321 --- /dev/null +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -0,0 +1,794 @@ +#include "stdafx.h" +#include "savestate_manager_dialog.h" +#include "custom_table_widget_item.h" +#include "game_list_delegate.h" +#include "qt_utils.h" +#include "game_list.h" +#include "gui_settings.h" +#include "progress_dialog.h" + +#include +#include +#include +#include +#include +#include +#include + +LOG_CHANNEL(gui_log, "GUI"); + +enum GameUserRole +{ + GameIndex = Qt::UserRole, + GamePixmapLoaded, + GamePixmap +}; + +savestate_manager_dialog::savestate_manager_dialog(std::shared_ptr gui_settings, const std::vector& games) + : QWidget() + , m_gui_settings(std::move(gui_settings)) + , m_game_info(games) +{ + // Nonspecific widget settings + setWindowTitle(tr("Savestate Manager")); + setObjectName("savestate_manager"); + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_StyledBackground); + + m_game_icon_size_index = m_gui_settings->GetValue(gui::ss_game_icon_size).toInt(); + + // Game chooser combo box + m_game_combo = new QComboBox(); + m_game_combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + + // Games Table + m_game_table = new game_list(); + m_game_table->setObjectName("savestate_manager_game_table"); + m_game_table->setShowGrid(false); + m_game_table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_table->verticalScrollBar()->installEventFilter(this); + m_game_table->verticalScrollBar()->setSingleStep(20); + m_game_table->horizontalScrollBar()->setSingleStep(20); + m_game_table->setItemDelegate(new game_list_delegate(m_game_table)); + m_game_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_game_table->setSelectionMode(QAbstractItemView::SingleSelection); + m_game_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_game_table->setColumnCount(static_cast(gui::savestate_game_list_columns::count)); + m_game_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + m_game_table->horizontalHeader()->setStretchLastSection(true); + m_game_table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + m_game_table->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_table->verticalHeader()->setVisible(false); + m_game_table->setAlternatingRowColors(true); + m_game_table->installEventFilter(this); + + auto add_game_column = [this](gui::savestate_game_list_columns col, const QString& header_text, const QString& action_text) + { + m_game_table->setHorizontalHeaderItem(static_cast(col), new QTableWidgetItem(header_text)); + m_game_column_acts.append(new QAction(action_text, this)); + }; + + add_game_column(gui::savestate_game_list_columns::icon, tr("Icon"), tr("Show Icons")); + add_game_column(gui::savestate_game_list_columns::name, tr("Game"), tr("Show Games")); + add_game_column(gui::savestate_game_list_columns::savestates, tr("Savestates"), tr("Show Savestates")); + + // Savestate Table + m_savestate_table = new game_list(); + m_savestate_table->setObjectName("savestate_manager_savestate_table"); + m_savestate_table->setShowGrid(false); + m_savestate_table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_savestate_table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + m_savestate_table->verticalScrollBar()->installEventFilter(this); + m_savestate_table->verticalScrollBar()->setSingleStep(20); + m_savestate_table->horizontalScrollBar()->setSingleStep(20); + m_savestate_table->setItemDelegate(new game_list_delegate(m_savestate_table)); + m_savestate_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_savestate_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_savestate_table->setColumnCount(static_cast(gui::savestate_list_columns::count)); + m_savestate_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + m_savestate_table->horizontalHeader()->setStretchLastSection(true); + m_savestate_table->verticalHeader()->setVisible(false); + m_savestate_table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + m_savestate_table->setContextMenuPolicy(Qt::CustomContextMenu); + m_savestate_table->setAlternatingRowColors(true); + m_savestate_table->installEventFilter(this); + + auto add_savestate_column = [this](gui::savestate_list_columns col, const QString& header_text, const QString& action_text) + { + m_savestate_table->setHorizontalHeaderItem(static_cast(col), new QTableWidgetItem(header_text)); + m_savestate_column_acts.append(new QAction(action_text, this)); + }; + + add_savestate_column(gui::savestate_list_columns::name, tr("Name"), tr("Show Names")); + add_savestate_column(gui::savestate_list_columns::compatible, tr("Compatible"), tr("Show Compatible")); + add_savestate_column(gui::savestate_list_columns::date, tr("Created"), tr("Show Created")); + add_savestate_column(gui::savestate_list_columns::path, tr("Path"), tr("Show Paths")); + + m_splitter = new QSplitter(); + m_splitter->addWidget(m_game_table); + m_splitter->addWidget(m_savestate_table); + + m_game_icon_size = gui_settings::SizeFromSlider(m_game_icon_size_index); + + QLabel* game_slider_label = new QLabel(); + game_slider_label->setText(tr("Game Icon Size: %0x%1").arg(m_game_icon_size.width()).arg(m_game_icon_size.height())); + + m_game_icon_slider = new QSlider(Qt::Horizontal); + m_game_icon_slider->setRange(0, gui::gl_max_slider_pos); + m_game_icon_slider->setValue(m_game_icon_size_index); + + // LAYOUTS + QGroupBox* choose_game = new QGroupBox(tr("Choose Game")); + QVBoxLayout* choose_layout = new QVBoxLayout(); + choose_layout->addWidget(m_game_combo); + choose_game->setLayout(choose_layout); + + QGroupBox* icon_settings = new QGroupBox(tr("Icon Options")); + QVBoxLayout* slider_layout = new QVBoxLayout(); + slider_layout->addWidget(game_slider_label); + slider_layout->addWidget(m_game_icon_slider); + icon_settings->setLayout(slider_layout); + + QVBoxLayout* options_layout = new QVBoxLayout(); + options_layout->addWidget(choose_game); + options_layout->addWidget(icon_settings); + options_layout->addStretch(); + + QHBoxLayout* all_layout = new QHBoxLayout(this); + all_layout->addLayout(options_layout); + all_layout->addWidget(m_splitter); + all_layout->setStretch(1, 1); + setLayout(all_layout); + + // Make connects + connect(m_game_icon_slider, &QSlider::valueChanged, this, [this, game_slider_label](int val) + { + m_game_icon_size_index = val; + m_game_icon_size = gui_settings::SizeFromSlider(val); + if (game_slider_label) + { + game_slider_label->setText(tr("Game Icon Size: %0x%1").arg(m_game_icon_size.width()).arg(m_game_icon_size.height())); + } + ResizeGameIcons(); + if (m_save_game_icon_size) + { + m_save_game_icon_size = false; + m_gui_settings->SetValue(gui::ss_game_icon_size, val); + } + }); + + connect(m_game_icon_slider, &QSlider::sliderReleased, this, [this]() + { + m_gui_settings->SetValue(gui::ss_game_icon_size, m_game_icon_slider->value()); + }); + + connect(m_game_icon_slider, &QSlider::actionTriggered, this, [this](int action) + { + if (action != QAbstractSlider::SliderNoAction && action != QAbstractSlider::SliderMove) + { // we only want to save on mouseclicks or slider release (the other connect handles this) + m_save_game_icon_size = true; // actionTriggered happens before the value was changed + } + }); + + connect(m_savestate_table, &QTableWidget::customContextMenuRequested, this, &savestate_manager_dialog::ShowSavestateTableContextMenu); + + connect(m_game_combo, &QComboBox::currentTextChanged, this, [this] + { + PopulateSavestateTable(); + }); + + connect(m_game_table, &QTableWidget::customContextMenuRequested, this, &savestate_manager_dialog::ShowGameTableContextMenu); + + connect(m_game_table, &QTableWidget::itemSelectionChanged, this, [this] + { + if (m_game_table->selectedItems().isEmpty()) + { + return; + } + QTableWidgetItem* item = m_game_table->item(m_game_table->selectedItems().first()->row(), static_cast(gui::savestate_game_list_columns::name)); + if (!item) + { + return; + } + m_game_combo->setCurrentText(item->text()); + }); + + connect(this, &savestate_manager_dialog::GameIconReady, this, [this](int index, const QPixmap& pixmap) + { + if (QTableWidgetItem* icon_item = m_game_table->item(index, static_cast(gui::savestate_game_list_columns::icon))) + { + icon_item->setData(Qt::DecorationRole, pixmap); + } + }); + + m_savestate_table->create_header_actions(m_savestate_column_acts, + [this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast(col)); }, + [this](int col, bool visible) { m_gui_settings->SetSavestateListColVisibility(static_cast(col), visible); }); + + m_game_table->create_header_actions(m_game_column_acts, + [this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast(col)); }, + [this](int col, bool visible) { m_gui_settings->SetSavestateGamelistColVisibility(static_cast(col), visible); }); + + RepaintUI(true); + + StartSavestateLoadThreads(); +} + +savestate_manager_dialog::~savestate_manager_dialog() +{ + WaitAndAbortGameRepaintThreads(); +} + +bool savestate_manager_dialog::LoadSavestateFolderToDB(std::unique_ptr&& game_savestates) +{ + ensure(!!game_savestates); + + if (game_savestates->title_id.empty()) + { + gui_log.error("Failed to load savestates. Path empty!"); + return false; + } + + const std::string dir_path = fs::get_config_dir() + "savestates/" + game_savestates->title_id + "/"; + + const QDir savestate_dir(QString::fromStdString(dir_path)); + const QFileInfoList file_list = savestate_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + + if (file_list.isEmpty()) + { + return false; + } + + // Populate game_savestates_data + game_savestates->savestates.resize(file_list.size()); + game_savestates->dir_path = dir_path; + + extern bool is_savestate_compatible(const std::string& filepath); + + for (usz id = 0; id < game_savestates->savestates.size(); ++id) + { + game_savestates->savestates[id].name = file_list[id].baseName(); + game_savestates->savestates[id].path = file_list[id].absoluteFilePath(); + game_savestates->savestates[id].date = file_list[id].birthTime(); + game_savestates->savestates[id].is_compatible = is_savestate_compatible(game_savestates->savestates[id].path.toStdString()); + } + + { + std::scoped_lock lock(m_savestate_db_mtx); + m_savestate_db.push_back(std::move(game_savestates)); + } + + return true; +} + +void savestate_manager_dialog::RepaintUI(bool restore_layout) +{ + if (m_gui_settings->GetValue(gui::m_enableUIColors).toBool()) + { + m_game_icon_color = m_gui_settings->GetValue(gui::ss_icon_color).value(); + } + else + { + m_game_icon_color = gui::utils::get_label_color("savestate_manager_icon_background_color", Qt::transparent, Qt::transparent); + } + + PopulateGameTable(); + + if (restore_layout && !restoreGeometry(m_gui_settings->GetValue(gui::ss_geometry).toByteArray())) + { + resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); + } + + if (restore_layout && !m_splitter->restoreState(m_gui_settings->GetValue(gui::ss_splitterState).toByteArray())) + { + const int width_left = m_splitter->width() * 0.4; + const int width_right = m_splitter->width() - width_left; + m_splitter->setSizes({ width_left, width_right }); + } + + PopulateSavestateTable(); + + const QByteArray game_table_state = m_gui_settings->GetValue(gui::ss_games_state).toByteArray(); + if (restore_layout && !m_game_table->horizontalHeader()->restoreState(game_table_state) && m_game_table->rowCount()) + { + // If no settings exist, resize to contents. (disabled) + //m_game_table->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + //m_game_table->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + } + + const QByteArray savestate_table_state = m_gui_settings->GetValue(gui::ss_savestate_state).toByteArray(); + if (restore_layout && !m_savestate_table->horizontalHeader()->restoreState(savestate_table_state) && m_savestate_table->rowCount()) + { + // If no settings exist, resize to contents. (disabled) + //m_savestate_table->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + //m_savestate_table->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + } + + if (restore_layout) + { + // Make sure the actions and the headers are synced + m_game_table->sync_header_actions(m_game_column_acts, [this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast(col)); }); + m_savestate_table->sync_header_actions(m_savestate_column_acts, [this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast(col)); }); + } + + // Show dialog and then paint gui in order to adjust headers correctly + show(); + ReadjustGameTable(); + ReadjustSavestateTable(); +} + +void savestate_manager_dialog::HandleRepaintUiRequest() +{ + const QSize window_size = size(); + const QByteArray splitter_state = m_splitter->saveState(); + const QByteArray game_table_state = m_game_table->horizontalHeader()->saveState(); + const QByteArray savestate_table_state = m_savestate_table->horizontalHeader()->saveState(); + + RepaintUI(false); + + m_splitter->restoreState(splitter_state); + m_game_table->horizontalHeader()->restoreState(game_table_state); + m_savestate_table->horizontalHeader()->restoreState(savestate_table_state); + + // Make sure the actions and the headers are synced + m_game_table->sync_header_actions(m_game_column_acts, [this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast(col)); }); + m_savestate_table->sync_header_actions(m_savestate_column_acts, [this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast(col)); }); + + resize(window_size); +} + +void savestate_manager_dialog::ResizeGameIcons() +{ + if (m_game_combo->count() <= 0) + return; + + WaitAndAbortGameRepaintThreads(); + + QPixmap placeholder(m_game_icon_size); + placeholder.fill(Qt::transparent); + + qRegisterMetaType>("QVector"); + for (int i = 0; i < m_game_table->rowCount(); ++i) + { + if (QTableWidgetItem* icon_item = m_game_table->item(i, static_cast(gui::savestate_game_list_columns::icon))) + { + icon_item->setData(Qt::DecorationRole, placeholder); + } + } + + ReadjustGameTable(); + + for (int i = 0; i < m_game_table->rowCount(); ++i) + { + if (movie_item* item = static_cast(m_game_table->item(i, static_cast(gui::savestate_game_list_columns::icon)))) + { + const qreal dpr = devicePixelRatioF(); + const int savestate_index = item->data(GameUserRole::GameIndex).toInt(); + const std::string icon_path = m_savestate_db[savestate_index]->game_icon_path; + + item->set_icon_load_func([this, icon_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) + { + if (cancel && cancel->load()) + { + return; + } + + QPixmap icon; + + if (movie_item* item = static_cast(m_game_table->item(index, static_cast(gui::savestate_game_list_columns::icon)))) + { + if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) + { + // Load game icon + if (!icon.load(QString::fromStdString(icon_path))) + { + gui_log.warning("Could not load savestate game icon from path %s", icon_path); + } + item->setData(GameUserRole::GamePixmapLoaded, true); + item->setData(GameUserRole::GamePixmap, icon); + } + else + { + icon = item->data(GameUserRole::GamePixmap).value(); + } + } + + if (cancel && cancel->load()) + { + return; + } + + QPixmap new_icon(icon.size() * dpr); + new_icon.setDevicePixelRatio(dpr); + new_icon.fill(m_game_icon_color); + + if (!icon.isNull()) + { + QPainter painter(&new_icon); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawPixmap(QPoint(0, 0), icon); + painter.end(); + } + + new_icon = new_icon.scaled(m_game_icon_size * dpr, Qt::KeepAspectRatio, Qt::TransformationMode::SmoothTransformation); + + if (!cancel || !cancel->load()) + { + Q_EMIT GameIconReady(index, new_icon); + } + }); + } + } +} + +void savestate_manager_dialog::ShowSavestateTableContextMenu(const QPoint& pos) +{ + const int row = m_savestate_table->currentRow(); + const QTableWidgetItem* path_item = m_savestate_table->item(row, static_cast(gui::savestate_list_columns::path)); + + if (!path_item) + { + return; + } + + QMenu* menu = new QMenu(); + QAction* show_savestate_dir = new QAction(tr("&Open Savestate Directory"), menu); + QAction* boot_savestate = new QAction(tr("&Boot Savestate"), menu); + QAction* delete_savestate = new QAction(tr("&Delete Savestate"), menu); + + if (const QTableWidgetItem* comp_item = m_savestate_table->item(row, static_cast(gui::savestate_list_columns::compatible))) + { + boot_savestate->setEnabled(comp_item->data(Qt::UserRole).toBool()); + } + + const int db_ind = m_game_combo->currentData().toInt(); + const std::string path = path_item->text().toStdString(); + + connect(show_savestate_dir, &QAction::triggered, this, [this, db_ind]() + { + gui::utils::open_dir(QString::fromStdString(m_savestate_db[db_ind]->dir_path)); + }); + + connect(boot_savestate, &QAction::triggered, this, [this, path]() + { + gui_log.notice("Booting savestate from savestate manager..."); + Q_EMIT RequestBoot(path); + }); + + connect(delete_savestate, &QAction::triggered, this, [this, path]() + { + gui_log.notice("Removing savestate '%s' from savestate manager...", path); + + if (QMessageBox::question(this, tr("Confirm Deletion"), tr("Delete savestate '%0'?").arg(QString::fromStdString(path))) != QMessageBox::Yes) + return; + + if (fs::remove_file(path)) + { + gui_log.success("Removed savestate '%s'", path); + StartSavestateLoadThreads(); // Reload the savestate list + } + else + { + gui_log.error("Failed to remove file '%s' (%s)", path, fs::g_tls_error); + QMessageBox::warning(this, tr("Deletion Failed!"), tr("Failed to delete savestate '%0'!").arg(QString::fromStdString(path))); + } + }); + + menu->addAction(boot_savestate); + menu->addAction(show_savestate_dir); + menu->addSeparator(); + menu->addAction(delete_savestate); + + menu->exec(m_savestate_table->viewport()->mapToGlobal(pos)); +} + +void savestate_manager_dialog::ShowGameTableContextMenu(const QPoint& pos) +{ + const int row = m_game_table->currentRow(); + + if (!m_game_table->item(row, static_cast(gui::savestate_game_list_columns::icon))) + { + return; + } + + QMenu* menu = new QMenu(); + QAction* remove_savestate_dir = new QAction(tr("&Remove All Savestates"), this); + QAction* show_savestate_dir = new QAction(tr("&Open Savestate Directory"), menu); + + const int db_ind = m_game_combo->currentData().toInt(); + + const QTableWidgetItem* name_item = m_game_table->item(row, static_cast(gui::savestate_game_list_columns::name)); + const QString name = name_item ? name_item->text() : ""; + + connect(remove_savestate_dir, &QAction::triggered, this, [this, name, db_ind]() + { + if (QMessageBox::question(this, tr("Delete Confirmation"), tr("Are you sure you want to delete the savestates for:\n%0?").arg(name), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) + { + const std::string path = m_savestate_db[db_ind]->dir_path; + ensure(path != (fs::get_config_dir() + "savestates/")); // Make sure we aren't deleting the root path by accident + fs::remove_all(path); // Remove the game's savestate folder + StartSavestateLoadThreads(); // Reload the savestate list + } + }); + connect(show_savestate_dir, &QAction::triggered, this, [this, db_ind]() + { + gui::utils::open_dir(QString::fromStdString(m_savestate_db[db_ind]->dir_path)); + }); + + menu->addAction(show_savestate_dir); + + if (!name.isEmpty()) + { + QAction* copy_name = new QAction(tr("&Copy Name"), menu); + connect(copy_name, &QAction::triggered, this, [this, name]() + { + QApplication::clipboard()->setText(name); + }); + menu->addAction(copy_name); + } + + menu->addSeparator(); + menu->addAction(remove_savestate_dir); + + menu->exec(m_game_table->viewport()->mapToGlobal(pos)); +} + +void savestate_manager_dialog::StartSavestateLoadThreads() +{ + WaitAndAbortGameRepaintThreads(); + + m_savestate_db.clear(); + + const QString savestate_path = QString::fromStdString(fs::get_config_dir() + "savestates/"); + const QDir savestate_dir(savestate_path); + const QStringList folder_list = savestate_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + const int count = folder_list.count(); + + if (count <= 0) + { + RepaintUI(true); + return; + } + + std::vector> game_data(count); + + qRegisterMetaType>("QVector"); + QList indices; + for (int i = 0; i < count; ++i) + { + indices.append(i); + + game_data[i] = std::make_unique(); + game_data[i]->title_id = folder_list[i].toStdString(); + + for (const game_info& gameinfo : m_game_info) + { + if (gameinfo && gameinfo->info.serial == game_data[i]->title_id) + { + game_data[i]->game_name = gameinfo->info.name; + game_data[i]->game_icon_path = gameinfo->info.icon_path; + break; + } + } + + if (game_data[i]->game_name.empty()) + { + game_data[i]->game_name = game_data[i]->title_id; + } + } + + QFutureWatcher future_watcher; + + progress_dialog progress_dialog(tr("Loading savestates"), tr("Loading savestates, please wait..."), tr("Cancel"), 0, 1, false, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + + connect(&future_watcher, &QFutureWatcher::progressRangeChanged, &progress_dialog, &QProgressDialog::setRange); + connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress_dialog, &QProgressDialog::setValue); + connect(&future_watcher, &QFutureWatcher::finished, this, [this]() { RepaintUI(true); }); + connect(&progress_dialog, &QProgressDialog::canceled, this, [this, &future_watcher]() + { + future_watcher.cancel(); + close(); // It's pointless to show an empty window + }); + + atomic_t error_count{}; + future_watcher.setFuture(QtConcurrent::map(indices, [this, &error_count, &game_data](const int& i) + { + gui_log.trace("Loading savestate dir: %s", game_data[i]->title_id); + + if (!LoadSavestateFolderToDB(std::move(game_data[i]))) + { + // TODO: add a way of showing the number of corrupted/invalid savestates in UI somewhere. + gui_log.error("Error occurred while parsing folder %s for savestates.", game_data[i]->title_id); + error_count++; + } + })); + + progress_dialog.exec(); + + future_watcher.waitForFinished(); + + if (error_count != 0) + { + gui_log.error("Failed to load %d of %d savestates!", error_count.load(), count); + } +} + +void savestate_manager_dialog::PopulateGameTable() +{ + WaitAndAbortGameRepaintThreads(); + + m_game_table->setSortingEnabled(false); // Disable sorting before using setItem calls + m_game_table->clearContents(); + m_game_table->setRowCount(static_cast(m_savestate_db.size())); + + m_game_combo->clear(); + m_game_combo->blockSignals(true); + + qRegisterMetaType>("QVector"); + QList indices; + for (usz i = 0; i < m_savestate_db.size(); ++i) + indices.append(static_cast(i)); + + QPixmap placeholder(m_game_icon_size); + placeholder.fill(Qt::transparent); + + for (int i = 0; i < indices.count(); ++i) + { + const QString name = QString::fromStdString(m_savestate_db[i]->game_name).simplified(); + const quint64 savestate_count = m_savestate_db[i]->savestates.size(); + + custom_table_widget_item* icon_item = new custom_table_widget_item; + icon_item->setData(Qt::DecorationRole, placeholder); + icon_item->setData(GameUserRole::GameIndex, i); + icon_item->setData(GameUserRole::GamePixmapLoaded, false); + icon_item->setData(GameUserRole::GamePixmap, QPixmap()); + + m_game_table->setItem(i, static_cast(gui::savestate_game_list_columns::icon), icon_item); + m_game_table->setItem(i, static_cast(gui::savestate_game_list_columns::name), new custom_table_widget_item(name)); + m_game_table->setItem(i, static_cast(gui::savestate_game_list_columns::savestates), new custom_table_widget_item(QString::number(savestate_count), Qt::UserRole, savestate_count)); + + m_game_combo->addItem(name, i); + } + + m_game_combo->model()->sort(0, Qt::AscendingOrder); + m_game_combo->blockSignals(false); + m_game_combo->setCurrentIndex(0); + + m_game_table->setSortingEnabled(true); // Enable sorting only after using setItem calls + + ResizeGameIcons(); + + gui::utils::resize_combo_box_view(m_game_combo); +} + +void savestate_manager_dialog::PopulateSavestateTable() +{ + if (m_game_combo->count() <= 0) + return; + + const auto& data = m_savestate_db[m_game_combo->currentData().toInt()]; + ensure(!!data); + + gui_log.trace("Populating Savestate Manager UI with %s %s", data->game_name, data->dir_path); + + const std::vector& savestates = data->savestates; + + m_savestate_table->clear_list(); + m_savestate_table->setRowCount(static_cast(savestates.size())); + m_savestate_table->setSortingEnabled(false); // Disable sorting before using setItem calls + + const QLocale locale{}; + + for (int i = 0; i < static_cast(savestates.size()); i++) + { + const savestate_data& savestate = savestates[i]; + m_savestate_table->setItem(i, static_cast(gui::savestate_list_columns::name), new custom_table_widget_item(savestate.name)); + m_savestate_table->setItem(i, static_cast(gui::savestate_list_columns::compatible), new custom_table_widget_item(savestate.is_compatible ? tr("Compatible") : tr("Not compatible"), Qt::UserRole, savestate.is_compatible)); + m_savestate_table->setItem(i, static_cast(gui::savestate_list_columns::date), new custom_table_widget_item(savestate.date.toString(), Qt::UserRole, savestate.date)); + m_savestate_table->setItem(i, static_cast(gui::savestate_list_columns::path), new custom_table_widget_item(savestate.path)); + } + + m_savestate_table->setSortingEnabled(true); // Re-enable sorting after using setItem calls +} + +void savestate_manager_dialog::ReadjustGameTable() const +{ + // Fixate vertical header and row height + m_game_table->verticalHeader()->setMinimumSectionSize(m_game_icon_size.height()); + m_game_table->verticalHeader()->setMaximumSectionSize(m_game_icon_size.height()); + m_game_table->resizeRowsToContents(); + + // Resize and fixate icon column + m_game_table->resizeColumnToContents(static_cast(gui::savestate_game_list_columns::icon)); + m_game_table->horizontalHeader()->setSectionResizeMode(static_cast(gui::savestate_game_list_columns::icon), QHeaderView::Fixed); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_table->resizeColumnToContents(static_cast(gui::savestate_game_list_columns::count) - 1); +} + +void savestate_manager_dialog::ReadjustSavestateTable() const +{ + // Fixate vertical header and row height + m_savestate_table->resizeRowsToContents(); + + // Shorten the last section to remove horizontal scrollbar if possible + m_savestate_table->resizeColumnToContents(static_cast(gui::savestate_list_columns::count) - 1); +} + +bool savestate_manager_dialog::eventFilter(QObject *object, QEvent *event) +{ + const bool is_savestate_scroll = object == m_savestate_table->verticalScrollBar(); + const bool is_savestate_table = object == m_savestate_table; + const bool is_game_scroll = object == m_game_table->verticalScrollBar(); + const bool is_game_table = object == m_game_table; + int zoom_val = 0; + + switch (event->type()) + { + case QEvent::Wheel: + { + QWheelEvent *wheelEvent = static_cast(event); + + if (wheelEvent->modifiers() & Qt::ControlModifier && (is_savestate_scroll || is_game_scroll)) + { + const QPoint numSteps = wheelEvent->angleDelta() / 8 / 15; // http://doc.qt.io/qt-5/qwheelevent.html#pixelDelta + zoom_val = numSteps.y(); + } + break; + } + case QEvent::KeyPress: + { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent && keyEvent->modifiers() == Qt::ControlModifier && (is_savestate_table || is_game_table)) + { + if (keyEvent->key() == Qt::Key_Plus) + { + zoom_val = 1; + } + else if (keyEvent->key() == Qt::Key_Minus) + { + zoom_val = -1; + } + } + break; + } + default: + break; + } + + if (zoom_val != 0) + { + if (m_game_icon_slider && (is_game_table || is_game_scroll)) + { + m_save_game_icon_size = true; + m_game_icon_slider->setSliderPosition(zoom_val + m_game_icon_slider->value()); + } + return true; + } + + return QWidget::eventFilter(object, event); +} + +void savestate_manager_dialog::closeEvent(QCloseEvent *event) +{ + // Save gui settings + m_gui_settings->SetValue(gui::ss_geometry, saveGeometry(), false); + m_gui_settings->SetValue(gui::ss_splitterState, m_splitter->saveState(), false); + m_gui_settings->SetValue(gui::ss_games_state, m_game_table->horizontalHeader()->saveState(), false); + m_gui_settings->SetValue(gui::ss_savestate_state, m_savestate_table->horizontalHeader()->saveState(), true); + + QWidget::closeEvent(event); +} + +void savestate_manager_dialog::WaitAndAbortGameRepaintThreads() +{ + for (int i = 0; i < m_game_table->rowCount(); i++) + { + if (movie_item* item = static_cast(m_game_table->item(i, static_cast(gui::savestate_game_list_columns::icon)))) + { + item->wait_for_icon_loading(true); + } + } +} diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.h b/rpcs3/rpcs3qt/savestate_manager_dialog.h new file mode 100644 index 0000000000..9c39f75765 --- /dev/null +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.h @@ -0,0 +1,85 @@ +#pragma once + +#include "game_list_base.h" + +#include +#include +#include + +#include + +class game_list; +class gui_settings; + +class savestate_manager_dialog : public QWidget +{ + Q_OBJECT + +public: + explicit savestate_manager_dialog(std::shared_ptr gui_settings, const std::vector& games); + ~savestate_manager_dialog() override; + void RepaintUI(bool restore_layout = true); + +public Q_SLOTS: + void HandleRepaintUiRequest(); + +private Q_SLOTS: + void ResizeGameIcons(); + void ShowSavestateTableContextMenu(const QPoint& pos); + void ShowGameTableContextMenu(const QPoint& pos); + +Q_SIGNALS: + void GameIconReady(int index, const QPixmap& pixmap); + void RequestBoot(const std::string& path); + +private: + struct savestate_data + { + QString name; + QString path; + QDateTime date; + bool is_compatible = false; + }; + + struct game_savestates_data + { + std::vector savestates; + std::string title_id; + std::string game_name; + std::string game_icon_path; + std::string dir_path; + }; + + bool LoadSavestateFolderToDB(std::unique_ptr&& game_savestates); + void StartSavestateLoadThreads(); + + void PopulateGameTable(); + void PopulateSavestateTable(); + + void ReadjustGameTable() const; + void ReadjustSavestateTable() const; + + void WaitAndAbortGameRepaintThreads(); + + void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *object, QEvent *event) override; + + std::shared_ptr m_gui_settings; + + std::vector m_game_info; + std::vector> m_savestate_db; //! Holds all the savestate information. + std::mutex m_savestate_db_mtx; + QComboBox* m_game_combo; //! Lets you choose a game + QSplitter* m_splitter; //! Contains the game and savestate tables + game_list* m_savestate_table; //! UI element to display savestate stuff. + game_list* m_game_table; //! UI element to display games. + + QList m_savestate_column_acts; + QList m_game_column_acts; + + int m_game_icon_size_index = 25; + QSize m_game_icon_size = QSize(m_game_icon_size_index, m_game_icon_size_index); + bool m_save_game_icon_size = false; + QSlider* m_game_icon_slider = nullptr; + QColor m_game_icon_color; +}; diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index 72a1e2d42c..1c525809ba 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -601,7 +601,6 @@ void trophy_manager_dialog::ResizeGameIcons() if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) { // Load game icon - const std::string icon_path = m_trophies_db[trophy_index]->path + "ICON0.PNG"; if (!icon.load(QString::fromStdString(icon_path))) { gui_log.warning("Could not load trophy game icon from path %s", icon_path); @@ -1110,6 +1109,8 @@ void trophy_manager_dialog::PopulateTrophyTable() return; auto& data = m_trophies_db[m_game_combo->currentData().toInt()]; + ensure(!!data); + gui_log.trace("Populating Trophy Manager UI with %s %s", data->game_name, data->path); const int all_trophies = data->trop_usr->GetTrophiesCount();