diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index d77ef974d..3a369775d 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -1,13 +1,16 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com> +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "gamelistmodel.h" +#include "qthost.h" +#include "qtutils.h" + +#include "core/system.h" + #include "common/file_system.h" #include "common/path.h" #include "common/string_util.h" -#include "core/system.h" -#include "qthost.h" -#include "qtutils.h" + #include <QtConcurrent/QtConcurrent> #include <QtCore/QDate> #include <QtCore/QDateTime> @@ -26,6 +29,11 @@ static constexpr int COVER_ART_HEIGHT = 512; static constexpr int COVER_ART_SPACING = 32; static constexpr int MIN_COVER_CACHE_SIZE = 256; +static std::string getMemoryCardIconCachePath() +{ + return Path::Combine(EmuFolders::Cache, "memcard_icons.cache"); +} + static int DPRScale(int size, float dpr) { return static_cast<int>(static_cast<float>(size) * dpr); @@ -114,15 +122,32 @@ const char* GameListModel::getColumnName(Column col) return s_column_names[static_cast<int>(col)]; } -GameListModel::GameListModel(float cover_scale, bool show_cover_titles, QObject* parent /* = nullptr */) - : QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles) +GameListModel::GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons, + QObject* parent /* = nullptr */) + : QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles), m_show_game_icons(show_game_icons), + m_memcard_icon_cache(getMemoryCardIconCachePath()), m_memcard_pixmap_cache(128) { loadCommonImages(); setCoverScale(cover_scale); setColumnDisplayNames(); + + if (m_show_game_icons) + m_memcard_icon_cache.Reload(); } + GameListModel::~GameListModel() = default; +void GameListModel::setShowGameIcons(bool enabled) +{ + m_show_game_icons = enabled; + + beginResetModel(); + m_memcard_pixmap_cache.Clear(); + if (enabled) + m_memcard_icon_cache.Reload(); + endResetModel(); +} + void GameListModel::setCoverScale(float scale) { if (m_cover_scale == scale) @@ -224,6 +249,31 @@ QString GameListModel::formatTimespan(time_t timespan) return qApp->translate("GameList", "%n minutes", "", minutes); } +const QPixmap& GameListModel::getIconForEntry(const GameList::Entry* ge) const +{ + // We only do this for discs/disc sets for now. + if (m_show_game_icons && (!ge->serial.empty() && (ge->IsDisc() || ge->IsDiscSet()))) + { + QPixmap* item = m_memcard_pixmap_cache.Lookup(ge->serial); + if (item) + return *item; + + const MemoryCardImage::IconFrame* icon = m_memcard_icon_cache.Lookup(ge->serial, ge->path); + if (icon) + { + const QImage image(reinterpret_cast<const uchar*>(icon->pixels), MemoryCardImage::ICON_WIDTH, + MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888); + return *m_memcard_pixmap_cache.Insert(ge->serial, QPixmap::fromImage(image)); + } + else + { + return *m_memcard_pixmap_cache.Insert(ge->serial, m_type_pixmaps[static_cast<u32>(ge->type)]); + } + } + + return m_type_pixmaps[static_cast<u32>(ge->type)]; +} + int GameListModel::getCoverArtWidth() const { return std::max(static_cast<int>(static_cast<float>(COVER_ART_WIDTH) * m_cover_scale), 1); @@ -407,8 +457,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const { case Column_Type: { - // TODO: Test for settings - return m_type_pixmaps[static_cast<u32>(ge->type)]; + return getIconForEntry(ge); } case Column_Region: @@ -455,6 +504,10 @@ QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int void GameListModel::refresh() { beginResetModel(); + + // Invalidate memcard LRU cache, forcing a re-query of the memcard timestamps. + m_memcard_pixmap_cache.Clear(); + endResetModel(); } diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index 7a1e5faf9..60e9b6b9a 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -1,10 +1,11 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com> +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once #include "core/game_database.h" #include "core/game_list.h" +#include "core/memory_card_icon_cache.h" #include "core/types.h" #include "common/heterogeneous_containers.h" @@ -46,7 +47,7 @@ public: static std::optional<Column> getColumnIdForName(std::string_view name); static const char* getColumnName(Column col); - GameListModel(float cover_scale, bool show_cover_titles, QObject* parent = nullptr); + GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons, QObject* parent = nullptr); ~GameListModel(); int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -66,6 +67,9 @@ public: bool getShowCoverTitles() const { return m_show_titles_for_covers; } void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; } + bool getShowGameIcons() const { return m_show_game_icons; } + void setShowGameIcons(bool enabled); + float getCoverScale() const { return m_cover_scale; } void setCoverScale(float scale); int getCoverArtWidth() const; @@ -84,10 +88,13 @@ private: void loadOrGenerateCover(const GameList::Entry* ge); void invalidateCoverForPath(const std::string& path); + const QPixmap& getIconForEntry(const GameList::Entry* ge) const; + static QString formatTimespan(time_t timespan); float m_cover_scale = 0.0f; bool m_show_titles_for_covers = false; + bool m_show_game_icons = false; std::array<QString, Column_Count> m_column_display_names; std::array<QPixmap, static_cast<int>(GameList::EntryType::Count)> m_type_pixmaps; @@ -98,4 +105,7 @@ private: QPixmap m_loading_pixmap; mutable LRUCache<std::string, QPixmap> m_cover_pixmap_cache; -}; \ No newline at end of file + + mutable MemoryCardIconCache m_memcard_icon_cache; + mutable LRUCache<std::string, QPixmap> m_memcard_pixmap_cache; +}; diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index 40034f488..35e987dc8 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -114,7 +114,8 @@ void GameListWidget::initialize() const float cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f); const bool show_cover_titles = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true); const bool merge_disc_sets = Host::GetBaseBoolSettingValue("UI", "GameListMergeDiscSets", true); - m_model = new GameListModel(cover_scale, show_cover_titles, this); + const bool show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true); + m_model = new GameListModel(cover_scale, show_cover_titles, show_game_icons, this); m_model->updateCacheSize(width(), height()); m_sort_model = new GameListSortModel(m_model); @@ -242,6 +243,11 @@ bool GameListWidget::isMergingDiscSets() const return m_sort_model->isMergingDiscSets(); } +bool GameListWidget::isShowingGameIcons() const +{ + return m_model->getShowGameIcons(); +} + void GameListWidget::refresh(bool invalidate_cache) { cancelRefresh(); @@ -476,6 +482,16 @@ void GameListWidget::setMergeDiscSets(bool enabled) emit layoutChange(); } +void GameListWidget::setShowGameIcons(bool enabled) +{ + if (m_model->getShowGameIcons() == enabled) + return; + + Host::SetBaseBoolSettingValue("UI", "GameListShowGameIcons", enabled); + Host::CommitBaseSettingChanges(); + m_model->setShowGameIcons(enabled); +} + void GameListWidget::updateToolbar() { const bool grid_view = isShowingGameGrid(); diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index 7f3bb4866..a7334804a 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -53,6 +53,7 @@ public: bool isShowingGameGrid() const; bool isShowingGridCoverTitles() const; bool isMergingDiscSets() const; + bool isShowingGameIcons() const; const GameList::Entry* getSelectedEntry() const; @@ -85,6 +86,7 @@ public Q_SLOTS: void showGameGrid(); void setShowCoverTitles(bool enabled); void setMergeDiscSets(bool enabled); + void setShowGameIcons(bool enabled); void gridZoomIn(); void gridZoomOut(); void gridIntScale(int int_scale); diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index e5e8925cf..f71aa9117 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1637,6 +1637,7 @@ void MainWindow::setupAdditionalUi() m_game_list_widget->initialize(); m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles()); m_ui.actionMergeDiscSets->setChecked(m_game_list_widget->isMergingDiscSets()); + m_ui.actionShowGameIcons->setChecked(m_game_list_widget->isShowingGameIcons()); if (s_use_central_widget) { m_ui.mainContainer = nullptr; // setCentralWidget() will delete this @@ -2096,6 +2097,7 @@ void MainWindow::connectSignals() SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets); + connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { if (isShowingGameList()) diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 9c7f436ae..1be6dea29 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -17,7 +17,7 @@ <string>DuckStation</string> </property> <property name="windowIcon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset> </property> <property name="unifiedTitleAndToolBarOnMac"> @@ -220,7 +220,8 @@ <addaction name="actionFullscreen"/> <addaction name="menuWindowSize"/> <addaction name="separator"/> - <addaction name="actionMergeDiscSets" /> + <addaction name="actionMergeDiscSets"/> + <addaction name="actionShowGameIcons"/> <addaction name="actionGridViewShowTitles"/> <addaction name="actionGridViewZoomIn"/> <addaction name="actionGridViewZoomOut"/> @@ -444,7 +445,7 @@ </action> <action name="actionGitHubRepository"> <property name="icon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/github.png</normaloff>:/icons/github.png</iconset> </property> <property name="text"> @@ -453,7 +454,7 @@ </action> <action name="actionIssueTracker"> <property name="icon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/IssueTracker.png</normaloff>:/icons/IssueTracker.png</iconset> </property> <property name="text"> @@ -462,7 +463,7 @@ </action> <action name="actionDiscordServer"> <property name="icon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/discord.png</normaloff>:/icons/discord.png</iconset> </property> <property name="text"> @@ -484,7 +485,7 @@ </action> <action name="actionAboutQt"> <property name="icon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/QT.png</normaloff>:/icons/QT.png</iconset> </property> <property name="text"> @@ -493,7 +494,7 @@ </action> <action name="actionAbout"> <property name="icon"> - <iconset resource="resources/resources.qrc"> + <iconset resource="resources/duckstation-qt.qrc"> <normaloff>:/icons/duck_64.png</normaloff>:/icons/duck_64.png</iconset> </property> <property name="text"> @@ -951,9 +952,20 @@ <string>Memory &Scanner</string> </property> </action> + <action name="actionShowGameIcons"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <property name="text"> + <string>Show Game Icons (List View)</string> + </property> + </action> </widget> <resources> - <include location="resources/resources.qrc"/> + <include location="resources/duckstation-qt.qrc"/> </resources> <connections/> </ui> diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index d6e4a27ca..78b5e1bb8 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -248,9 +248,10 @@ bool QtHost::SaveGameSettings(SettingsInterface* sif, bool delete_if_empty) return true; } -QIcon QtHost::GetAppIcon() +const QIcon& QtHost::GetAppIcon() { - return QIcon(QStringLiteral(":/icons/duck.png")); + static QIcon icon = QIcon(QStringLiteral(":/icons/duck.png")); + return icon; } std::optional<bool> QtHost::DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector<u8>* data) diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 40977268b..22afadf49 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -270,7 +270,7 @@ QString GetAppNameAndVersion(); QString GetAppConfigSuffix(); /// Returns the main application icon. -QIcon GetAppIcon(); +const QIcon& GetAppIcon(); /// Returns the base path for resources. This may be : prefixed, if we're using embedded resources. QString GetResourcesBasePath();