diff --git a/REUSE.toml b/REUSE.toml index 662987611..7a7e4bb38 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -7,8 +7,8 @@ path = [ "CMakeSettings.json", ".github/FUNDING.yml", ".github/shadps4.png", - ".github/workflows/scripts/update_translation.sh", - ".github/workflows/update_translation.yml", + ".github/workflows/scripts/update_translation.sh", + ".github/workflows/update_translation.yml", ".gitmodules", "dist/MacOSBundleInfo.plist.in", "dist/net.shadps4.shadPS4.desktop", @@ -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", @@ -71,7 +72,7 @@ path = [ "src/images/youtube.svg", "src/shadps4.qrc", "src/shadps4.rc", - "src/qt_gui/translations/update_translation.sh", + "src/qt_gui/translations/update_translation.sh", ] precedence = "aggregate" SPDX-FileCopyrightText = "shadPS4 Emulator Project" diff --git a/src/images/favorite_icon.png b/src/images/favorite_icon.png new file mode 100644 index 000000000..743eb0fbe Binary files /dev/null and b/src/images/favorite_icon.png differ diff --git a/src/qt_gui/game_grid_frame.cpp b/src/qt_gui/game_grid_frame.cpp index 8a5219da1..dda73fa17 100644 --- a/src/qt_gui/game_grid_frame.cpp +++ b/src/qt_gui/game_grid_frame.cpp @@ -36,6 +36,7 @@ GameGridFrame::GameGridFrame(std::shared_ptr gui_settings, connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) { 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); }); } @@ -89,10 +90,13 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from this->crtColumn = -1; QVector m_games_; this->clearContents(); - if (fromSearch) + if (fromSearch) { + SortByFavorite(&m_games_search); m_games_ = m_games_search; - else + } else { + SortByFavorite(&(m_game_info->m_games)); m_games_ = m_game_info->m_games; + } m_games_shared = std::make_shared>(m_games_); icon_size = m_gui_settings->GetValue(gui::gg_icon_size).toInt(); // update icon size for resize event. @@ -111,14 +115,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. @@ -226,3 +237,43 @@ 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); + QList list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite = list.contains(serialStr); + + 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(QVector* game_list) { + std::sort(game_list->begin(), game_list->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); + QList list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite_a = list.contains(serialStr_a); + bool isFavorite_b = list.contains(serialStr_b); + 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..0a12deb1c 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(QVector* game_list); 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 45a9a4810..e4c40b4f9 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -16,6 +16,7 @@ GameListFrame::GameListFrame(std::shared_ptr gui_settings, : QTableWidget(parent), m_gui_settings(std::move(gui_settings)), m_game_info(game_info_get), m_compat_info(compat_info_get) { icon_size = m_gui_settings->GetValue(gui::gl_icon_size).toInt(); + last_favorite = ""; this->setShowGrid(false); this->setEditTriggers(QAbstractItemView::NoEditTriggers); this->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -30,9 +31,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,14 +41,18 @@ 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::currentCellChanged, this, &GameListFrame::onCurrentCellChanged); @@ -65,18 +69,24 @@ 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, - m_gui_settings, this, true); + int changedFavorite = m_gui_context_menus.RequestGameMenu( + pos, m_game_info->m_games, m_compat_info, m_gui_settings, this, true); + if (changedFavorite) { + last_favorite = m_game_info->m_games[this->currentRow()].serial; + PopulateGameList(false); + } }); connect(this, &QTableWidget::cellClicked, this, [=, this](int row, int column) { @@ -84,6 +94,19 @@ GameListFrame::GameListFrame(std::shared_ptr gui_settings, auto url_issues = "https://github.com/shadps4-emu/shadps4-game-compatibility/issues/"; QDesktopServices::openUrl( QUrl(url_issues + m_game_info->m_games[row].compatibility.issue_number)); + } else if (column == 10) { + last_favorite = m_game_info->m_games[row].serial; + QString serialStr = QString::fromStdString(last_favorite); + QList list = + gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite = list.contains(serialStr); + if (isFavorite) { + list.removeOne(serialStr); + } else { + list.append(serialStr); + } + m_gui_settings->SetValue(gui::favorites_list, gui_settings::List2Var(list)); + PopulateGameList(false); } }); } @@ -118,10 +141,7 @@ 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)); @@ -130,6 +150,11 @@ 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); + + if (m_game_info->m_games[i].serial == last_favorite && !isInitialPopulation) { + this->setCurrentCell(i, 10); + } m_game_info->m_games[i].compatibility = m_compat_info->GetCompatibilityInfo(m_game_info->m_games[i].serial); @@ -227,20 +252,50 @@ 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); + QList list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite_a = list.contains(serialStr_a); + bool isFavorite_b = list.contains(serialStr_b); + 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, @@ -391,6 +446,35 @@ 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); + QList list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite = list.contains(serialStr); + + 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); + } +} + QString GameListFrame::GetPlayTime(const std::string& serial) { QString playTime; const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); @@ -423,4 +507,4 @@ QString GameListFrame::GetPlayTime(const std::string& serial) { QTableWidgetItem* GameListFrame::GetCurrentItem() { return m_current_item; -} \ No newline at end of file +} diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h index f70d73054..d1e065864 100644 --- a/src/qt_gui/game_list_frame.h +++ b/src/qt_gui/game_list_frame.h @@ -42,11 +42,13 @@ public Q_SLOTS: 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 +57,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; @@ -63,6 +66,7 @@ public: std::shared_ptr m_compat_info; int icon_size; + std::string last_favorite; static float parseAsFloat(const std::string& str, const int& offset) { return std::stof(str.substr(0, str.size() - offset)); @@ -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 ba82da261..6c384c4bc 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 @@ -30,13 +31,13 @@ class GuiContextMenus : public QObject { Q_OBJECT public: - void RequestGameMenu(const QPoint& pos, QVector& m_games, - std::shared_ptr m_compat_info, - std::shared_ptr settings, QTableWidget* widget, - bool isList) { + int 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; + int changedFavorite = 0; if (isList) { itemID = widget->currentRow(); } else { @@ -45,7 +46,7 @@ public: // Do not show the menu if no item is selected if (itemID < 0 || itemID >= m_games.size()) { - return; + return changedFavorite; } // Setup menu. @@ -65,11 +66,22 @@ public: menu.addMenu(openFolderMenu); + QString serialStr = QString::fromStdString(m_games[itemID].serial); + QList list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list)); + bool isFavorite = list.contains(serialStr); + QAction* toggleFavorite; + + if (isFavorite) { + toggleFavorite = new QAction(tr("Remove from Favorites"), widget); + } else { + toggleFavorite = new QAction(tr("Add to Favorites"), 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(toggleFavorite); menu.addAction(&createShortcut); menu.addAction(&openCheats); menu.addAction(&openSfoViewer); @@ -130,7 +142,7 @@ public: // Show menu. auto selected = menu.exec(global_pos); if (!selected) { - return; + return changedFavorite; } if (selected == openGameFolder) { @@ -303,6 +315,16 @@ public: } } + if (selected == toggleFavorite) { + if (isFavorite) { + list.removeOne(serialStr); + } else { + list.append(serialStr); + } + m_gui_settings->SetValue(gui::favorites_list, gui_settings::List2Var(list)); + changedFavorite = 1; + } + if (selected == &openCheats) { QString gameName = QString::fromStdString(m_games[itemID].name); QString gameSerial = QString::fromStdString(m_games[itemID].serial); @@ -588,6 +610,7 @@ public: QUrl(url_issues + m_games[itemID].compatibility.issue_number)); } } + return changedFavorite; } int GetRowIndex(QTreeWidget* treeWidget, QTreeWidgetItem* item) { diff --git a/src/qt_gui/gui_settings.h b/src/qt_gui/gui_settings.h index 0fa807d70..4c1eafc95 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); @@ -41,6 +42,10 @@ const gui_value gl_backgroundMusicVolume = gui_value(game_list, "backgroundMusic const gui_value gg_icon_size = gui_value(game_grid, "icon_size", 69); const gui_value gg_slider_pos = gui_value(game_grid, "slider_pos", 0); +// favorites list +const gui_value favorites_list = + gui_value(favorites, "favoritesList", QVariant::fromValue(QList())); + } // namespace gui class gui_settings : public settings { diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 9379519c2..166a31d72 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -561,10 +561,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(); 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