From 327144c87426c8a516f3daf1ccfb83e67c434d73 Mon Sep 17 00:00:00 2001 From: David Antunes Date: Thu, 5 Jun 2025 23:43:02 +0100 Subject: [PATCH] Favorites in the game list (#2649) Changed how favorites are saved to match PR #2984. Adjusted the favorite icon size. Fixed bug where favorites were inconsistent when changing to list mode. Instantly sort list when adding or removing a favorite. Co-authored-by: David Antunes --- REUSE.toml | 1 + src/images/favorite_icon.png | Bin 0 -> 1170 bytes src/qt_gui/game_grid_frame.cpp | 53 +++++++++++++++- src/qt_gui/game_grid_frame.h | 3 + src/qt_gui/game_list_frame.cpp | 111 +++++++++++++++++++++++++++++---- src/qt_gui/game_list_frame.h | 6 ++ src/qt_gui/gui_context_menus.h | 11 ++++ src/qt_gui/gui_settings.h | 1 + src/qt_gui/main_window.cpp | 13 ++-- src/shadps4.qrc | 1 + 10 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 src/images/favorite_icon.png diff --git a/REUSE.toml b/REUSE.toml index 662987611..6968022c0 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -29,6 +29,7 @@ path = [ "src/images/discord.png", "src/images/dump_icon.png", "src/images/exit_icon.png", + "src/images/favorite_icon.png", "src/images/file_icon.png", "src/images/trophy_icon.png", "src/images/flag_china.png", diff --git a/src/images/favorite_icon.png b/src/images/favorite_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..743eb0fbead5ed435fa55837b63218ccb8cb3ff0 GIT binary patch literal 1170 zcmV;D1a13?P)<{0000nFfjlC000000000000000 z0Q~&?;^N`}000dQ4FCWDXlQ8m_V$;Tm&eD)b#--4O-u$PE!E<8U+sL^P>Eb$@{qZHYr}%fBySz{6+Wu`1$wv`S|$x z`1q3t;C2821FK0yK~!ko?U~t@qaYMTgYy{YDKXI)`Tt*RL==Za1&gN+Z&gd#Ys(4O zijV#TOQ>20|F+f>vcB zB2I;@$gN|DQkjW}QkjWR6-9uEOGF^N3sCXqB7+L}QiDn1w#sEu^ZdJDLK&)gIiY)kcDM)R%E^qH* z1%Yu>Cx;DH2Xa^BKUO_MtuLm0?ulerj&m6YG?Mvqx-4g0b3JA&i0c zWSuNvZU|#^J3k{Oei25(Q1C}eeTeepVNgnYgu=<_UQ7>D28PQ1rl*9Y+SfZ7i(PI8 zav>CVTP1p?{EUu*pV%WhVxsZB75f-c?v)U{B&x>=3BSAxEe3wc)3P%MPw9ORQek1v zvjYX!eEYcvd1Hvl*_8y-UUcJkVxQ<#G92N0w?rB^UYtkL$HIl;g?C`}dLmrS{@zvc zw~t4|1>p_gs(T__%eLfMR?C0A@y4R_^p1VY`zjU)6eag&{8hX@ge$K4J`v0@C*qah z`Kl?xrM<_ip_mBAOXG=`IS-d{b7x%+c+uvIJ6^JbU+_t|jOMj!+Q4JLH280hcuA9R z6%zyqXnUE~_lWkv9Pz^80$sR@_k&slq2!tB0u$Sur(b=z@(z(s5FY;KG{M;UlfQ0} zaNX+(!t};ZTRJ^du*+x7S+~f^5IsOq&1pvEY>ILD;-xtAD5?5&v93zyERIN+>y|V- zqR{m0Lv$41ndRDHjo0*BiRNTqZdZJ3f?o27(enB$d<2PFPpmzfMtmN{PJ zgQDdhcmz^NFbPMvfR!BFzP%n}vWF{37%BwbYZ&8o)zBuzm`77(tXo;gz7e4P-^BLDyZ07*qoM6N<$f;XNNfdBvi literal 0 HcmV?d00001 diff --git a/src/qt_gui/game_grid_frame.cpp b/src/qt_gui/game_grid_frame.cpp index 66679dc71..5a98e9c72 100644 --- a/src/qt_gui/game_grid_frame.cpp +++ b/src/qt_gui/game_grid_frame.cpp @@ -34,7 +34,8 @@ GameGridFrame::GameGridFrame(std::shared_ptr gui_settings, connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this, &GameGridFrame::RefreshGridBackgroundImage); connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) { - m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, m_compat_info, this, false); + m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, m_compat_info, m_gui_settings, this, false); + PopulateGameGrid(m_game_info->m_games, false); }); } @@ -88,6 +89,7 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from this->crtColumn = -1; QVector m_games_; this->clearContents(); + SortByFavorite(); if (fromSearch) m_games_ = m_games_search; else @@ -110,14 +112,21 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from for (int i = 0; i < m_games_.size(); i++) { QWidget* widget = new QWidget(); QVBoxLayout* layout = new QVBoxLayout(); - QLabel* image_label = new QLabel(); + + QWidget* image_container = new QWidget(); + image_container->setFixedSize(icon_size, icon_size); + + QLabel* image_label = new QLabel(image_container); QImage icon = m_games_[gameCounter].icon.scaled( QSize(icon_size, icon_size), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); image_label->setFixedSize(icon.width(), icon.height()); image_label->setPixmap(QPixmap::fromImage(icon)); + image_label->move(0, 0); + SetFavoriteIcon(image_container, m_games_, gameCounter); + QLabel* name_label = new QLabel(QString::fromStdString(m_games_[gameCounter].serial)); name_label->setAlignment(Qt::AlignHCenter); - layout->addWidget(image_label); + layout->addWidget(image_container); layout->addWidget(name_label); // Resizing of font-size. @@ -225,3 +234,41 @@ void GameGridFrame::resizeEvent(QResizeEvent* event) { bool GameGridFrame::IsValidCellSelected() { return validCellSelected; } + +void GameGridFrame::SetFavoriteIcon(QWidget* parentWidget, QVector m_games_, + int gameCounter) { + QString serialStr = QString::fromStdString(m_games_[gameCounter].serial); + bool isFavorite = m_gui_settings->GetValue(gui::favorites, serialStr, false).toBool(); + + QLabel* label = new QLabel(parentWidget); + label->setPixmap( + QPixmap(":images/favorite_icon.png") + .scaled(icon_size / 3.8, icon_size / 3.8, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + label->move(icon_size - icon_size / 4, 2); + label->raise(); + label->setVisible(isFavorite); + label->setObjectName("favoriteIcon"); +} + +void GameGridFrame::SortByFavorite() { + std::sort( + m_game_info->m_games.begin(), m_game_info->m_games.end(), + [this](const GameInfo& a, const GameInfo& b) { return this->CompareWithFavorite(a, b); }); +} + +bool GameGridFrame::CompareWithFavorite(GameInfo a, GameInfo b) { + std::string serial_a = a.serial; + std::string serial_b = b.serial; + QString serialStr_a = QString::fromStdString(a.serial); + QString serialStr_b = QString::fromStdString(b.serial); + bool isFavorite_a = m_gui_settings->GetValue(gui::favorites, serialStr_a, false).toBool(); + bool isFavorite_b = m_gui_settings->GetValue(gui::favorites, serialStr_b, false).toBool(); + if (isFavorite_a != isFavorite_b) { + return isFavorite_a; + } else { + std::string name_a = a.name, name_b = b.name; + std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower); + std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower); + return name_a < name_b; + } +} diff --git a/src/qt_gui/game_grid_frame.h b/src/qt_gui/game_grid_frame.h index 22d278a21..a0f6bc747 100644 --- a/src/qt_gui/game_grid_frame.h +++ b/src/qt_gui/game_grid_frame.h @@ -39,6 +39,8 @@ private: int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation std::filesystem::path m_current_game_path; // Track current game path to detect changes std::shared_ptr m_gui_settings; + void SetFavoriteIcon(QWidget* parentWidget, QVector m_games_, int gameCounter); + bool CompareWithFavorite(GameInfo a, GameInfo b); public: explicit GameGridFrame(std::shared_ptr gui_settings, @@ -47,6 +49,7 @@ public: QWidget* parent = nullptr); void PopulateGameGrid(QVector m_games, bool fromSearch); bool IsValidCellSelected(); + void SortByFavorite(); bool cellClicked = false; int icon_size; diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp index dd10e0f8b..fc3c813e1 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -30,9 +30,8 @@ GameListFrame::GameListFrame(std::shared_ptr gui_settings, this->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); this->horizontalHeader()->setHighlightSections(false); this->horizontalHeader()->setSortIndicatorShown(true); - this->horizontalHeader()->setStretchLastSection(true); this->setContextMenuPolicy(Qt::CustomContextMenu); - this->setColumnCount(10); + this->setColumnCount(11); this->setColumnWidth(1, 300); // Name this->setColumnWidth(2, 140); // Compatibility this->setColumnWidth(3, 120); // Serial @@ -41,16 +40,24 @@ GameListFrame::GameListFrame(std::shared_ptr gui_settings, this->setColumnWidth(6, 90); // Size this->setColumnWidth(7, 90); // Version this->setColumnWidth(8, 120); // Play Time + this->setColumnWidth(10, 90); // Favorite QStringList headers; headers << tr("Icon") << tr("Name") << tr("Compatibility") << tr("Serial") << tr("Region") - << tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path"); + << tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path") + << tr("Favorite"); this->setHorizontalHeaderLabels(headers); this->horizontalHeader()->setSortIndicatorShown(true); this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); this->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed); this->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Fixed); + this->horizontalHeader()->setSectionResizeMode(9, QHeaderView::Stretch); + this->horizontalHeader()->setSectionResizeMode(10, QHeaderView::Fixed); PopulateGameList(); + connect(this, &QTableWidget::cellClicked, this, [=, this](int row, int column) { + ToggleFavorite(row, column); + PopulateGameList(false); + }); connect(this, &QTableWidget::currentCellChanged, this, &GameListFrame::onCurrentCellChanged); connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this, &GameListFrame::RefreshListBackgroundImage); @@ -65,17 +72,20 @@ GameListFrame::GameListFrame(std::shared_ptr gui_settings, SortNameDescending(columnIndex); this->horizontalHeader()->setSortIndicator(columnIndex, Qt::DescendingOrder); ListSortedAsc = false; + sortColumn = columnIndex; } else { SortNameAscending(columnIndex); this->horizontalHeader()->setSortIndicator(columnIndex, Qt::AscendingOrder); ListSortedAsc = true; + sortColumn = columnIndex; } this->clearContents(); PopulateGameList(false); }); connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) { - m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, m_compat_info, this, true); + m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, m_compat_info, m_gui_settings, this, true); + PopulateGameList(false); }); connect(this, &QTableWidget::cellClicked, this, [=, this](int row, int column) { @@ -116,11 +126,8 @@ void GameListFrame::PopulateGameList(bool isInitialPopulation) { this->setRowCount(m_game_info->m_games.size()); ResizeIcons(icon_size); - - if (isInitialPopulation) { - SortNameAscending(1); // Column 1 = Name - ResizeIcons(icon_size); - } + + ApplyLastSorting(isInitialPopulation); for (int i = 0; i < m_game_info->m_games.size(); i++) { SetTableItem(i, 1, QString::fromStdString(m_game_info->m_games[i].name)); @@ -129,6 +136,7 @@ void GameListFrame::PopulateGameList(bool isInitialPopulation) { SetTableItem(i, 5, QString::fromStdString(m_game_info->m_games[i].fw)); SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].size)); SetTableItem(i, 7, QString::fromStdString(m_game_info->m_games[i].version)); + SetFavoriteIcon(i, 10); m_game_info->m_games[i].compatibility = m_compat_info->GetCompatibilityInfo(m_game_info->m_games[i].serial); @@ -226,20 +234,49 @@ void GameListFrame::resizeEvent(QResizeEvent* event) { RefreshListBackgroundImage(); } +bool GameListFrame::CompareWithFavorite(GameInfo a, GameInfo b, int columnIndex, bool ascending) { + std::string serial_a = a.serial; + std::string serial_b = b.serial; + QString serialStr_a = QString::fromStdString(a.serial); + QString serialStr_b = QString::fromStdString(b.serial); + bool isFavorite_a = m_gui_settings->GetValue(gui::favorites, serialStr_a, false).toBool(); + bool isFavorite_b = m_gui_settings->GetValue(gui::favorites, serialStr_b, false).toBool(); + if (isFavorite_a != isFavorite_b) { + return isFavorite_a; + } else if (ascending) { + return CompareStringsAscending(a, b, columnIndex); + } else { + return CompareStringsDescending(a, b, columnIndex); + } +} + void GameListFrame::SortNameAscending(int columnIndex) { std::sort(m_game_info->m_games.begin(), m_game_info->m_games.end(), - [columnIndex](const GameInfo& a, const GameInfo& b) { - return CompareStringsAscending(a, b, columnIndex); + [this, columnIndex](const GameInfo& a, const GameInfo& b) { + return this->CompareWithFavorite(a, b, columnIndex, true); }); } void GameListFrame::SortNameDescending(int columnIndex) { std::sort(m_game_info->m_games.begin(), m_game_info->m_games.end(), - [columnIndex](const GameInfo& a, const GameInfo& b) { - return CompareStringsDescending(a, b, columnIndex); + [this, columnIndex](const GameInfo& a, const GameInfo& b) { + return this->CompareWithFavorite(a, b, columnIndex, false); }); } +void GameListFrame::ApplyLastSorting(bool isInitialPopulation) { + if (isInitialPopulation) { + SortNameAscending(1); // Column 1 = Name + ResizeIcons(icon_size); + } else if (ListSortedAsc) { + SortNameAscending(sortColumn); + ResizeIcons(icon_size); + } else { + SortNameDescending(sortColumn); + ResizeIcons(icon_size); + } +} + void GameListFrame::ResizeIcons(int iconSize) { for (int index = 0; auto& game : m_game_info->m_games) { QImage scaledPixmap = game.icon.scaled(QSize(iconSize, iconSize), Qt::KeepAspectRatio, @@ -390,6 +427,54 @@ void GameListFrame::SetRegionFlag(int row, int column, QString itemStr) { this->setCellWidget(row, column, widget); } +void GameListFrame::SetFavoriteIcon(int row, int column) { + QString serialStr = QString::fromStdString(m_game_info->m_games[row].serial); + bool isFavorite = m_gui_settings->GetValue(gui::favorites, serialStr, false).toBool(); + + QTableWidgetItem* item = new QTableWidgetItem(); + QImage scaledPixmap = QImage(":images/favorite_icon.png"); + + scaledPixmap = scaledPixmap.scaledToHeight(this->columnWidth(column) / 2.5); + scaledPixmap = scaledPixmap.scaledToWidth(this->columnWidth(column) / 2.5); + QWidget* widget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(widget); + QLabel* label = new QLabel(widget); + label->setPixmap(QPixmap::fromImage(scaledPixmap)); + label->setObjectName("favoriteIcon"); + label->setVisible(isFavorite); + + layout->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + widget->setLayout(layout); + this->setItem(row, column, item); + this->setCellWidget(row, column, widget); + + if (column > 0) { + this->horizontalHeader()->setSectionResizeMode(column - 1, QHeaderView::Stretch); + } +} + +void GameListFrame::ToggleFavorite(int row, int column) { + if (column != 10) { + return; + } + + QWidget* cellWidget = this->cellWidget(row, column); + if (!cellWidget) { + return; + } + + QLabel* label = cellWidget->findChild("favoriteIcon"); + if (!label) { + return; + } + + QString serialStr = QString::fromStdString(m_game_info->m_games[row].serial); + bool isFavorite = m_gui_settings->GetValue(gui::favorites, serialStr, false).toBool(); + m_gui_settings->SetValue(gui::favorites, serialStr, !isFavorite, true); + label->setVisible(!isFavorite); +} + QString GameListFrame::GetPlayTime(const std::string& serial) { QString playTime; const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h index f70d73054..abdadd722 100644 --- a/src/qt_gui/game_list_frame.h +++ b/src/qt_gui/game_list_frame.h @@ -38,15 +38,18 @@ public Q_SLOTS: void PlayBackgroundMusic(QTableWidgetItem* item); void onCurrentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn); + void ToggleFavorite(int row, int column); private: void SetTableItem(int row, int column, QString itemStr); void SetRegionFlag(int row, int column, QString itemStr); + void SetFavoriteIcon(int row, int column); void SetCompatibilityItem(int row, int column, CompatibilityEntry entry); QString GetPlayTime(const std::string& serial); QList m_columnActs; GameInfoClass* game_inf_get = nullptr; bool ListSortedAsc = true; + int sortColumn = 1; QTableWidgetItem* m_current_item = nullptr; int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation std::filesystem::path m_current_game_path; // Track current game path to detect changes @@ -55,6 +58,7 @@ private: public: void PopulateGameList(bool isInitialPopulation = true); void ResizeIcons(int iconSize); + void ApplyLastSorting(bool isInitialPopulation); QTableWidgetItem* GetCurrentItem(); QImage backgroundImage; GameListUtils m_game_list_utils; @@ -130,4 +134,6 @@ public: return false; } } + + bool CompareWithFavorite(GameInfo a, GameInfo b, int columnIndex, bool ascending); }; diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index 46a40c5cd..484870ad3 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -16,6 +16,7 @@ #include "common/scm_rev.h" #include "compatibility_info.h" #include "game_info.h" +#include "gui_settings.h" #include "trophy_viewer.h" #ifdef Q_OS_WIN @@ -32,8 +33,10 @@ class GuiContextMenus : public QObject { public: void RequestGameMenu(const QPoint& pos, QVector& m_games, std::shared_ptr m_compat_info, + std::shared_ptr settings, QTableWidget* widget, bool isList) { QPoint global_pos = widget->viewport()->mapToGlobal(pos); + std::shared_ptr m_gui_settings = std::move(settings); int itemID = 0; if (isList) { itemID = widget->currentRow(); @@ -63,11 +66,13 @@ public: menu.addMenu(openFolderMenu); + QAction addToFavorites(tr("Add/Remove Favorite"), widget); QAction createShortcut(tr("Create Shortcut"), widget); QAction openCheats(tr("Cheats / Patches"), widget); QAction openSfoViewer(tr("SFO Viewer"), widget); QAction openTrophyViewer(tr("Trophy Viewer"), widget); + menu.addAction(&addToFavorites); menu.addAction(&createShortcut); menu.addAction(&openCheats); menu.addAction(&openSfoViewer); @@ -301,6 +306,12 @@ public: } } + if (selected == &addToFavorites) { + QString serialStr = QString::fromStdString(m_games[itemID].serial); + bool isFavorite = m_gui_settings->GetValue(gui::favorites, serialStr, false).toBool(); + m_gui_settings->SetValue(gui::favorites, serialStr, !isFavorite, true); + } + if (selected == &openCheats) { QString gameName = QString::fromStdString(m_games[itemID].name); QString gameSerial = QString::fromStdString(m_games[itemID].serial); diff --git a/src/qt_gui/gui_settings.h b/src/qt_gui/gui_settings.h index da5542956..3ebb0f4c9 100644 --- a/src/qt_gui/gui_settings.h +++ b/src/qt_gui/gui_settings.h @@ -12,6 +12,7 @@ const QString general_settings = "general_settings"; const QString main_window = "main_window"; const QString game_list = "game_list"; const QString game_grid = "game_grid"; +const QString favorites = "favorites"; // general const gui_value gen_checkForUpdates = gui_value(general_settings, "checkForUpdates", false); diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index c6da49182..9ba48b9b0 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -563,10 +563,8 @@ void MainWindow::CreateConnects() { m_game_grid_frame->hide(); m_elf_viewer->hide(); m_game_list_frame->show(); - if (m_game_list_frame->item(0, 0) == nullptr) { - m_game_list_frame->clearContents(); - m_game_list_frame->PopulateGameList(); - } + m_game_list_frame->clearContents(); + m_game_list_frame->PopulateGameList(); isTableList = true; m_gui_settings->SetValue(gui::gl_mode, 0); int slider_pos = m_gui_settings->GetValue(gui::gl_slider_pos).toInt(); @@ -843,6 +841,13 @@ void MainWindow::CreateConnects() { } void MainWindow::StartGame() { + // Ignore favorite column + if (m_game_list_frame->currentItem()->column() == 10) { + m_game_list_frame->ToggleFavorite(m_game_list_frame->currentItem()->row(), + m_game_list_frame->currentItem()->column()); + return; + } + BackgroundMusicPlayer::getInstance().stopMusic(); QString gamePath = ""; int table_mode = m_gui_settings->GetValue(gui::gl_mode).toInt(); diff --git a/src/shadps4.qrc b/src/shadps4.qrc index 2aee394c8..707fc89b0 100644 --- a/src/shadps4.qrc +++ b/src/shadps4.qrc @@ -36,6 +36,7 @@ images/KBM.png images/fullscreen_icon.png images/refreshlist_icon.png + images/favorite_icon.png images/trophy_icon.png