diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index 99eaa2b96..2ff16b600 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -2,9 +2,67 @@ #include "common/string_util.h" #include "core/system.h" #include +#include static constexpr std::array s_column_names = { - {"Type", "Code", "Title", "File Title", "Size", "Region", "Compatibility"}}; + {"Type", "Code", "Title", "File Title", "Size", "Region", "Compatibility", "Cover"}}; + +static constexpr int COVER_ART_WIDTH = 512; +static constexpr int COVER_ART_HEIGHT = 512; +static constexpr int COVER_ART_SPACING = 32; + +static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height) +{ + if (pm->width() == expected_width && pm->height() == expected_height) + return; + + *pm = pm->scaled(expected_width, expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (pm->width() == expected_width && pm->height() == expected_height) + return; + + int xoffs = 0; + int yoffs = 0; + if (pm->width() < expected_width) + xoffs = (expected_width - pm->width()) / 2; + if (pm->height() < expected_height) + yoffs = (expected_height - pm->height()) / 2; + + QPixmap padded_image(expected_width, expected_height); + padded_image.fill(Qt::transparent); + QPainter painter; + if (painter.begin(&padded_image)) + { + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.drawPixmap(xoffs, yoffs, *pm); + painter.setCompositionMode(QPainter::CompositionMode_Destination); + painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0)); + painter.end(); + } + + *pm = padded_image; +} + +static QPixmap createPlaceholderImage(int width, int height, float scale, const std::string& title) +{ + QPixmap pm(QStringLiteral(":/icons/cover-placeholder.png")); + if (pm.isNull()) + return QPixmap(width, height); + + resizeAndPadPixmap(&pm, width, height); + QPainter painter; + if (painter.begin(&pm)) + { + QFont font; + font.setPointSize(std::max(static_cast(32.0f * scale), 1)); + painter.setFont(font); + painter.setPen(Qt::white); + + painter.drawText(QRect(0, 0, width, height), Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title)); + painter.end(); + } + + return pm; +} std::optional GameListModel::getColumnIdForName(std::string_view name) { @@ -30,6 +88,30 @@ GameListModel::GameListModel(GameList* game_list, QObject* parent /* = nullptr * } GameListModel::~GameListModel() = default; +void GameListModel::setCoverScale(float scale) +{ + if (m_cover_scale == scale) + return; + + m_cover_pixmap_cache.clear(); + m_cover_scale = scale; +} + +int GameListModel::getCoverArtWidth() const +{ + return std::max(static_cast(static_cast(COVER_ART_WIDTH) * m_cover_scale), 1); +} + +int GameListModel::getCoverArtHeight() const +{ + return std::max(static_cast(static_cast(COVER_ART_HEIGHT) * m_cover_scale), 1); +} + +int GameListModel::getCoverArtSpacing() const +{ + return std::max(static_cast(static_cast(COVER_ART_SPACING) * m_cover_scale), 1); +} + int GameListModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) @@ -78,6 +160,14 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const case Column_Size: return QString("%1 MB").arg(static_cast(ge.total_size) / 1048576.0, 0, 'f', 2); + case Column_Cover: + { + if (m_show_titles_for_covers) + return QString::fromStdString(ge.title); + else + return {}; + } + default: return {}; } @@ -94,6 +184,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return QString::fromStdString(ge.code); case Column_Title: + case Column_Cover: return QString::fromStdString(ge.title); case Column_FileTitle: @@ -155,6 +246,29 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const ge.compatibility_rating)]; } + case Column_Cover: + { + auto it = m_cover_pixmap_cache.find(ge.path); + if (it != m_cover_pixmap_cache.end()) + return it->second; + + QPixmap image; + std::string path = m_game_list->GetCoverImagePathForEntry(&ge); + if (!path.empty()) + { + image = QPixmap(QString::fromStdString(path)); + if (!image.isNull()) + resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight()); + } + + if (image.isNull()) + image = createPlaceholderImage(getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, ge.title); + + m_cover_pixmap_cache.emplace(ge.path, image); + return image; + } + break; + default: return {}; } diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index edf04892e..3ff3209d1 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -3,8 +3,10 @@ #include "frontend-common/game_list.h" #include #include +#include #include #include +#include class GameListModel final : public QAbstractTableModel { @@ -20,6 +22,7 @@ public: Column_Size, Column_Region, Column_Compatibility, + Column_Cover, Column_Count }; @@ -43,11 +46,22 @@ public: bool lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column, bool ascending) const; + bool getShowCoverTitles() const { return m_show_titles_for_covers; } + void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; } + + float getCoverScale() const { return m_cover_scale; } + void setCoverScale(float scale); + int getCoverArtWidth() const; + int getCoverArtHeight() const; + int getCoverArtSpacing() const; + private: void loadCommonImages(); void setColumnDisplayNames(); GameList* m_game_list; + float m_cover_scale = 1.0f; + bool m_show_titles_for_covers = false; std::array m_column_display_names; @@ -60,4 +74,5 @@ private: QPixmap m_region_us_pixmap; std::array(GameListCompatibilityRating::Count)> m_compatibiliy_pixmaps; + mutable std::unordered_map m_cover_pixmap_cache; }; \ No newline at end of file diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index f43c81eff..95049cf39 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -7,6 +7,7 @@ #include "qtutils.h" #include #include +#include #include #include @@ -42,11 +43,14 @@ void GameListWidget::initialize(QtHostInterface* host_interface) connect(m_host_interface, &QtHostInterface::gameListRefreshed, this, &GameListWidget::onGameListRefreshed); - m_table_model = new GameListModel(m_game_list, this); - m_table_sort_model = new GameListSortModel(m_table_model); - m_table_sort_model->setSourceModel(m_table_model); + m_model = new GameListModel(m_game_list, this); + m_model->setCoverScale(host_interface->GetFloatSettingValue("UI", "GameListCoverArtScale", 0.45f)); + m_model->setShowCoverTitles(host_interface->GetBoolSettingValue("UI", "GameListShowCoverTitles", true)); + + m_sort_model = new GameListSortModel(m_model); + m_sort_model->setSourceModel(m_model); m_table_view = new QTableView(this); - m_table_view->setModel(m_table_sort_model); + m_table_view->setModel(m_sort_model); m_table_view->setSortingEnabled(true); m_table_view->setSelectionMode(QAbstractItemView::SingleSelection); m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -73,19 +77,59 @@ void GameListWidget::initialize(QtHostInterface* host_interface) &GameListWidget::onTableViewHeaderSortIndicatorChanged); insertWidget(0, m_table_view); - setCurrentIndex(0); + + m_list_view = new GameListGridListView(this); + m_list_view->setModel(m_sort_model); + m_list_view->setModelColumn(GameListModel::Column_Cover); + m_list_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_list_view->setViewMode(QListView::IconMode); + m_list_view->setResizeMode(QListView::Adjust); + m_list_view->setUniformItemSizes(true); + m_list_view->setContextMenuPolicy(Qt::CustomContextMenu); + m_list_view->setFrameStyle(QFrame::NoFrame); + m_list_view->setSpacing(m_model->getCoverArtSpacing()); + updateListFont(); + + connect(m_list_view->selectionModel(), &QItemSelectionModel::currentChanged, this, + &GameListWidget::onSelectionModelCurrentChanged); + connect(m_list_view, &GameListGridListView::zoomIn, this, &GameListWidget::listZoomIn); + connect(m_list_view, &GameListGridListView::zoomOut, this, &GameListWidget::listZoomOut); + connect(m_list_view, &QListView::doubleClicked, this, &GameListWidget::onListViewItemDoubleClicked); + connect(m_list_view, &QListView::customContextMenuRequested, this, &GameListWidget::onListViewContextMenuRequested); + + insertWidget(1, m_list_view); + + if (m_host_interface->GetBoolSettingValue("UI", "GameListGridView", false)) + setCurrentIndex(1); + else + setCurrentIndex(0); resizeTableViewColumnsToFit(); } +bool GameListWidget::isShowingGameList() const +{ + return currentIndex() == 0; +} + +bool GameListWidget::isShowingGameGrid() const +{ + return currentIndex() == 1; +} + +bool GameListWidget::getShowGridCoverTitles() const +{ + return m_model->getShowCoverTitles(); +} + void GameListWidget::onGameListRefreshed() { - m_table_model->refresh(); + m_model->refresh(); } void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous) { - const QModelIndex source_index = m_table_sort_model->mapToSource(current); + const QModelIndex source_index = m_sort_model->mapToSource(current); if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) { emit entrySelected(nullptr); @@ -98,7 +142,7 @@ void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, void GameListWidget::onTableViewItemDoubleClicked(const QModelIndex& index) { - const QModelIndex source_index = m_table_sort_model->mapToSource(index); + const QModelIndex source_index = m_sort_model->mapToSource(index); if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) return; @@ -115,13 +159,35 @@ void GameListWidget::onTableViewContextMenuRequested(const QPoint& point) emit entryContextMenuRequested(m_table_view->mapToGlobal(point), entry); } +void GameListWidget::onListViewItemDoubleClicked(const QModelIndex& index) +{ + const QModelIndex source_index = m_sort_model->mapToSource(index); + if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) + return; + + const GameListEntry& entry = m_game_list->GetEntries().at(source_index.row()); + emit entryDoubleClicked(&entry); +} + +void GameListWidget::onListViewContextMenuRequested(const QPoint& point) +{ + const GameListEntry* entry = getSelectedEntry(); + if (!entry) + return; + + emit entryContextMenuRequested(m_list_view->mapToGlobal(point), entry); +} + void GameListWidget::onTableViewHeaderContextMenuRequested(const QPoint& point) { QMenu menu; for (int column = 0; column < GameListModel::Column_Count; column++) { - QAction* action = menu.addAction(m_table_model->getColumnDisplayName(column)); + if (column == GameListModel::Column_Cover) + continue; + + QAction* action = menu.addAction(m_model->getColumnDisplayName(column)); action->setCheckable(true); action->setChecked(!m_table_view->isColumnHidden(column)); connect(action, &QAction::toggled, [this, column](bool enabled) { @@ -139,6 +205,66 @@ void GameListWidget::onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder) saveTableViewColumnSortSettings(); } +void GameListWidget::listZoom(float delta) +{ + static constexpr float MIN_SCALE = 0.1f; + static constexpr float MAX_SCALE = 2.0f; + + const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE); + m_host_interface->SetFloatSettingValue("UI", "GameListCoverArtScale", new_scale); + m_model->setCoverScale(new_scale); + updateListFont(); + + m_model->refresh(); +} + +void GameListWidget::listZoomIn() +{ + listZoom(0.05f); +} + +void GameListWidget::listZoomOut() +{ + listZoom(-0.05f); +} + +void GameListWidget::showGameList() +{ + if (currentIndex() == 0) + return; + + m_host_interface->SetBoolSettingValue("UI", "GameListGridView", false); + setCurrentIndex(0); + resizeTableViewColumnsToFit(); +} + +void GameListWidget::showGameGrid() +{ + if (currentIndex() == 1) + return; + + m_host_interface->SetBoolSettingValue("UI", "GameListGridView", true); + setCurrentIndex(1); +} + +void GameListWidget::setShowCoverTitles(bool enabled) +{ + if (m_model->getShowCoverTitles() == enabled) + return; + + m_host_interface->SetBoolSettingValue("UI", "GameListShowCoverTitles", enabled); + m_model->setShowCoverTitles(enabled); + if (isShowingGameGrid()) + m_model->refresh(); +} + +void GameListWidget::updateListFont() +{ + QFont font; + font.setPointSizeF(16.0f * m_model->getCoverScale()); + m_list_view->setFont(font); +} + void GameListWidget::resizeEvent(QResizeEvent* event) { QStackedWidget::resizeEvent(event); @@ -193,7 +319,7 @@ void GameListWidget::loadTableViewColumnSortSettings() .value_or(DEFAULT_SORT_COLUMN); const bool sort_descending = m_host_interface->GetBoolSettingValue("GameListTableView", "SortDescending", DEFAULT_SORT_DESCENDING); - m_table_sort_model->sort(sort_column, sort_descending ? Qt::DescendingOrder : Qt::AscendingOrder); + m_sort_model->sort(sort_column, sort_descending ? Qt::DescendingOrder : Qt::AscendingOrder); } void GameListWidget::saveTableViewColumnSortSettings() @@ -212,17 +338,53 @@ void GameListWidget::saveTableViewColumnSortSettings() const GameListEntry* GameListWidget::getSelectedEntry() const { - const QItemSelectionModel* selection_model = m_table_view->selectionModel(); - if (!selection_model->hasSelection()) - return nullptr; + if (currentIndex() == 0) + { + const QItemSelectionModel* selection_model = m_table_view->selectionModel(); + if (!selection_model->hasSelection()) + return nullptr; - const QModelIndexList selected_rows = selection_model->selectedRows(); - if (selected_rows.empty()) - return nullptr; + const QModelIndexList selected_rows = selection_model->selectedRows(); + if (selected_rows.empty()) + return nullptr; - const QModelIndex source_index = m_table_sort_model->mapToSource(selected_rows[0]); - if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) - return nullptr; + const QModelIndex source_index = m_sort_model->mapToSource(selected_rows[0]); + if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) + return nullptr; - return &m_game_list->GetEntries().at(source_index.row()); + return &m_game_list->GetEntries().at(source_index.row()); + } + else + { + const QItemSelectionModel* selection_model = m_list_view->selectionModel(); + if (!selection_model->hasSelection()) + return nullptr; + + const QModelIndex source_index = m_sort_model->mapToSource(selection_model->currentIndex()); + if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) + return nullptr; + + return &m_game_list->GetEntries().at(source_index.row()); + } +} + +GameListGridListView::GameListGridListView(QWidget* parent /*= nullptr*/) : QListView(parent) {} + +void GameListGridListView::wheelEvent(QWheelEvent* e) +{ + if (e->modifiers() & Qt::ControlModifier) + { + int dy = e->angleDelta().y(); + if (dy != 0) + { + if (dy < 0) + zoomOut(); + else + zoomIn(); + + return; + } + } + + QListView::wheelEvent(e); } diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index 6d5c33f1e..2aba47573 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -10,6 +11,21 @@ class GameListSortModel; class QtHostInterface; +class GameListGridListView : public QListView +{ + Q_OBJECT + +public: + GameListGridListView(QWidget* parent = nullptr); + +Q_SIGNALS: + void zoomOut(); + void zoomIn(); + +protected: + void wheelEvent(QWheelEvent* e); +}; + class GameListWidget : public QStackedWidget { Q_OBJECT @@ -20,6 +36,11 @@ public: void initialize(QtHostInterface* host_interface); + bool isShowingGameList() const; + bool isShowingGameGrid() const; + + bool getShowGridCoverTitles() const; + Q_SIGNALS: void entrySelected(const GameListEntry* entry); void entryDoubleClicked(const GameListEntry* entry); @@ -32,6 +53,15 @@ private Q_SLOTS: void onTableViewContextMenuRequested(const QPoint& point); void onTableViewHeaderContextMenuRequested(const QPoint& point); void onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder); + void onListViewItemDoubleClicked(const QModelIndex& index); + void onListViewContextMenuRequested(const QPoint& point); + +public Q_SLOTS: + void showGameList(); + void showGameGrid(); + void setShowCoverTitles(bool enabled); + void listZoomIn(); + void listZoomOut(); protected: void resizeEvent(QResizeEvent* event); @@ -44,11 +74,14 @@ private: void saveTableViewColumnVisibilitySettings(int column); void loadTableViewColumnSortSettings(); void saveTableViewColumnSortSettings(); + void listZoom(float delta); + void updateListFont(); QtHostInterface* m_host_interface = nullptr; GameList* m_game_list = nullptr; - GameListModel* m_table_model = nullptr; - GameListSortModel* m_table_sort_model = nullptr; + GameListModel* m_model = nullptr; + GameListSortModel* m_sort_model = nullptr; QTableView* m_table_view = nullptr; + GameListGridListView* m_list_view = nullptr; }; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index fcfac49b4..541eb6c16 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -335,6 +335,15 @@ void MainWindow::onViewGameListActionTriggered() if (m_emulation_running) m_host_interface->pauseSystem(true); switchToGameListView(); + m_game_list_widget->showGameList(); +} + +void MainWindow::onViewGameGridActionTriggered() +{ + if (m_emulation_running) + m_host_interface->pauseSystem(true); + switchToGameListView(); + m_game_list_widget->showGameGrid(); } void MainWindow::onViewSystemDisplayTriggered() @@ -490,6 +499,8 @@ void MainWindow::setupAdditionalUi() m_status_frame_time_widget->setFixedSize(190, 16); m_status_frame_time_widget->hide(); + m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles()); + updateDebugMenuVisibility(); for (u32 i = 0; i < static_cast(CPUExecutionMode::Count); i++) @@ -597,6 +608,11 @@ void MainWindow::updateEmulationActions(bool starting, bool running) m_ui.statusBar->clearMessage(); } +bool MainWindow::isShowingGameList() const +{ + return m_ui.mainContainer->currentIndex() == 0; +} + void MainWindow::switchToGameListView() { m_ui.mainContainer->setCurrentIndex(0); @@ -671,6 +687,7 @@ void MainWindow::connectSignals() connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled); connect(m_ui.actionViewStatusBar, &QAction::toggled, this, &MainWindow::onViewStatusBarActionToggled); connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered); + connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered); connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered); connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered); connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered); @@ -678,6 +695,15 @@ void MainWindow::connectSignals() connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); + 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()) + m_game_list_widget->listZoomIn(); + }); + connect(m_ui.actionGridViewZoomOut, &QAction::triggered, m_game_list_widget, [this]() { + if (isShowingGameList()) + m_game_list_widget->listZoomOut(); + }); connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError, Qt::BlockingQueuedConnection); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index c0bd4a3f6..8fb73089c 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -67,6 +67,7 @@ private Q_SLOTS: void onViewToolbarActionToggled(bool checked); void onViewStatusBarActionToggled(bool checked); void onViewGameListActionTriggered(); + void onViewGameGridActionTriggered(); void onViewSystemDisplayTriggered(); void onGitHubRepositoryActionTriggered(); void onIssueTrackerActionTriggered(); @@ -91,6 +92,7 @@ private: void connectSignals(); void addThemeToMenu(const QString& name, const QString& key); void updateEmulationActions(bool starting, bool running); + bool isShowingGameList() const; void switchToGameListView(); void switchToEmulationView(); void saveStateToConfig(); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 939af6210..f8a6e0dfa 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -14,7 +14,7 @@ DuckStation - + :/icons/duck.png:/icons/duck.png @@ -42,7 +42,7 @@ Change Disc - + :/icons/media-optical.png:/icons/media-optical.png @@ -60,7 +60,7 @@ Cheats - + :/icons/conical-flask-red.png:/icons/conical-flask-red.png @@ -69,7 +69,7 @@ Load State - + :/icons/document-open.png:/icons/document-open.png @@ -78,7 +78,7 @@ Save State - + :/icons/document-save.png:/icons/document-save.png @@ -115,7 +115,7 @@ - + @@ -183,7 +183,12 @@ + + + + + @@ -236,7 +241,7 @@ - + :/icons/drive-optical.png:/icons/drive-optical.png @@ -245,7 +250,7 @@ - + :/icons/drive-removable-media.png:/icons/drive-removable-media.png @@ -254,7 +259,7 @@ - + :/icons/folder-open.png:/icons/folder-open.png @@ -263,7 +268,7 @@ - + :/icons/view-refresh.png:/icons/view-refresh.png @@ -272,7 +277,7 @@ - + :/icons/system-shutdown.png:/icons/system-shutdown.png @@ -281,7 +286,7 @@ - + :/icons/view-refresh.png:/icons/view-refresh.png @@ -293,7 +298,7 @@ true - + :/icons/media-playback-pause.png:/icons/media-playback-pause.png @@ -302,7 +307,7 @@ - + :/icons/document-open.png:/icons/document-open.png @@ -311,7 +316,7 @@ - + :/icons/document-save.png:/icons/document-save.png @@ -334,7 +339,7 @@ - + :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png @@ -343,7 +348,7 @@ - + :/icons/input-gaming.png:/icons/input-gaming.png @@ -352,7 +357,7 @@ - + :/icons/applications-other.png:/icons/applications-other.png @@ -361,7 +366,7 @@ - + :/icons/video-display.png:/icons/video-display.png @@ -370,7 +375,7 @@ - + :/icons/antialias-icon.png:/icons/antialias-icon.png @@ -379,7 +384,7 @@ - + :/icons/applications-graphics.png:/icons/applications-graphics.png @@ -388,7 +393,7 @@ - + :/icons/view-fullscreen.png:/icons/view-fullscreen.png @@ -427,7 +432,7 @@ - + :/icons/media-optical.png:/icons/media-optical.png @@ -436,7 +441,7 @@ - + :/icons/conical-flask-red.png:/icons/conical-flask-red.png @@ -445,7 +450,7 @@ - + :/icons/audio-card.png:/icons/audio-card.png @@ -454,7 +459,7 @@ - + :/icons/folder-open.png:/icons/folder-open.png @@ -463,7 +468,7 @@ - + :/icons/applications-system.png:/icons/applications-system.png @@ -472,7 +477,7 @@ - + :/icons/applications-development.png:/icons/applications-development.png @@ -481,7 +486,7 @@ - + :/icons/edit-find.png:/icons/edit-find.png @@ -490,7 +495,7 @@ - + :/icons/preferences-system.png:/icons/preferences-system.png @@ -601,7 +606,7 @@ - + :/icons/camera-photo.png:/icons/camera-photo.png @@ -610,7 +615,7 @@ - + :/icons/media-flash-24.png:/icons/media-flash-24.png @@ -619,7 +624,7 @@ - + :/icons/media-playback-start.png:/icons/media-playback-start.png @@ -669,9 +674,41 @@ Memory &Card Editor + + + Game Grid + + + + + true + + + true + + + Show Titles (Grid View) + + + + + Zoom In (Grid View) + + + Ctrl++ + + + + + Zoom Out (Grid View) + + + Ctrl+- + + - + diff --git a/src/duckstation-qt/resources/icons/cover-placeholder.png b/src/duckstation-qt/resources/icons/cover-placeholder.png new file mode 100644 index 000000000..18dc446fe Binary files /dev/null and b/src/duckstation-qt/resources/icons/cover-placeholder.png differ diff --git a/src/duckstation-qt/resources/resources.qrc b/src/duckstation-qt/resources/resources.qrc index 72a268891..fa40983fc 100644 --- a/src/duckstation-qt/resources/resources.qrc +++ b/src/duckstation-qt/resources/resources.qrc @@ -23,6 +23,7 @@ icons/camera-video@2x.png icons/conical-flask-red.png icons/conical-flask-red@2x.png + icons/cover-placeholder.png icons/document-open.png icons/document-open@2x.png icons/document-save.png