From 95e085584c37037e24c25c678ced8f5e9fcfd27d Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 5 Jun 2025 19:06:00 +1000 Subject: [PATCH] Qt: Move list/grid setup to subclasses --- src/duckstation-qt/gamelistwidget.cpp | 551 +++++++++++++------------- src/duckstation-qt/gamelistwidget.h | 71 ++-- src/duckstation-qt/mainwindow.cpp | 10 +- 3 files changed, 320 insertions(+), 312 deletions(-) diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index 043703fcd..728a7dfe3 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -120,14 +120,16 @@ const char* GameListModel::getColumnName(Column col) return s_column_names[static_cast(col)]; } -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_pixmap_cache(MIN_COVER_CACHE_SIZE) +GameListModel::GameListModel(QObject* parent) + : QAbstractTableModel(parent), m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE) { + m_cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f); + m_show_titles_for_covers = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true); + m_show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true); + loadCommonImages(); - setCoverScale(cover_scale); setColumnDisplayNames(); + updateCoverScale(); if (m_show_game_icons) GameList::ReloadMemcardTimestampCache(); @@ -153,9 +155,17 @@ void GameListModel::setCoverScale(float scale) if (m_cover_scale == scale) return; - m_cover_pixmap_cache.Clear(); m_cover_scale = scale; + Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", scale); + Host::CommitBaseSettingChanges(); + updateCoverScale(); +} + +void GameListModel::updateCoverScale() +{ + m_cover_pixmap_cache.Clear(); + const qreal dpr = qApp->devicePixelRatio(); QImage loading_image; @@ -185,7 +195,8 @@ void GameListModel::setCoverScale(float scale) m_placeholder_image.fill(QColor(0, 0, 0, 0)); } - emit coverScaleChanged(); + emit coverScaleChanged(m_cover_scale); + refresh(); } void GameListModel::refreshCovers() @@ -1111,25 +1122,6 @@ private: GameListSortModel* m_sort_model; }; -class GameListGridDelegate : public QStyledItemDelegate -{ -public: - GameListGridDelegate(bool show_titles) : m_show_titles(show_titles) {} - -protected: - void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override - { - QStyledItemDelegate::initStyleOption(option, index); - if (!m_show_titles) - option->features &= ~QStyleOptionViewItem::HasDisplay; - // else - // option->features |= QStyleOptionViewItem::WrapText; - } - -private: - bool m_show_titles = false; -}; - } // namespace GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent) @@ -1140,11 +1132,10 @@ GameListWidget::~GameListWidget() = default; 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); - 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 = new GameListModel(this); + connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged); m_sort_model = new GameListSortModel(m_model); m_sort_model->setSourceModel(m_model); @@ -1163,9 +1154,20 @@ void GameListWidget::initialize() QString::fromUtf8(Settings::GetDiscRegionName(static_cast(region)))); } + m_list_view = new GameListListView(m_model, m_sort_model, m_ui.stack); + m_ui.stack->insertWidget(0, m_list_view); + + m_grid_view = new GameListGridView(m_model, m_sort_model, m_ui.stack); + m_ui.stack->insertWidget(1, m_grid_view); + + m_empty_widget = new QWidget(m_ui.stack); + m_empty_ui.setupUi(m_empty_widget); + m_empty_ui.supportedFormats->setText(qApp->translate("GameListWidget", SUPPORTED_FORMATS_STRING)); + m_ui.stack->insertWidget(2, m_empty_widget); + connect(m_ui.viewGameList, &QPushButton::clicked, this, &GameListWidget::showGameList); connect(m_ui.viewGameGrid, &QPushButton::clicked, this, &GameListWidget::showGameGrid); - connect(m_ui.gridScale, &QSlider::valueChanged, this, &GameListWidget::gridIntScale); + connect(m_ui.gridScale, &QSlider::valueChanged, m_grid_view, &GameListGridView::setZoomPct); connect(m_ui.viewGridTitles, &QPushButton::toggled, this, &GameListWidget::setShowCoverTitles); connect(m_ui.viewMergeDiscSets, &QPushButton::toggled, this, &GameListWidget::setMergeDiscSets); connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) { @@ -1179,72 +1181,18 @@ void GameListWidget::initialize() [this](const QString& text) { m_sort_model->setFilterName(text.toStdString()); }); connect(m_ui.searchText, &QLineEdit::returnPressed, this, &GameListWidget::onSearchReturnPressed); - GameListCenterIconStyleDelegate* center_icon_delegate = new GameListCenterIconStyleDelegate(this); - m_list_view = new QTableView(m_ui.stack); - m_list_view->setModel(m_sort_model); - m_list_view->setSortingEnabled(true); - m_list_view->setSelectionMode(QAbstractItemView::SingleSelection); - m_list_view->setSelectionBehavior(QAbstractItemView::SelectRows); - m_list_view->setContextMenuPolicy(Qt::CustomContextMenu); - m_list_view->setAlternatingRowColors(true); - m_list_view->setShowGrid(false); - m_list_view->setCurrentIndex({}); - m_list_view->horizontalHeader()->setHighlightSections(false); - m_list_view->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); - m_list_view->verticalHeader()->hide(); - m_list_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - m_list_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); - m_list_view->setItemDelegateForColumn(GameListModel::Column_Icon, center_icon_delegate); - m_list_view->setItemDelegateForColumn(GameListModel::Column_Region, center_icon_delegate); - m_list_view->setItemDelegateForColumn(GameListModel::Column_Achievements, - new GameListAchievementsStyleDelegate(this, m_model, m_sort_model)); - - loadTableViewColumnVisibilitySettings(); - loadTableViewColumnSortSettings(); - connect(m_list_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &GameListWidget::onSelectionModelCurrentChanged); - connect(m_list_view, &QTableView::activated, this, &GameListWidget::onTableViewItemActivated); - connect(m_list_view, &QTableView::customContextMenuRequested, this, &GameListWidget::onTableViewContextMenuRequested); - connect(m_list_view->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, - &GameListWidget::onTableViewHeaderContextMenuRequested); - connect(m_list_view->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, - &GameListWidget::onTableViewHeaderSortIndicatorChanged); - - m_ui.stack->insertWidget(0, m_list_view); - - m_grid_view = new GameListGridListView(m_model, m_ui.stack); - m_grid_view->setModel(m_sort_model); - m_grid_view->setModelColumn(GameListModel::Column_Cover); - m_grid_view->setSelectionMode(QAbstractItemView::SingleSelection); - m_grid_view->setViewMode(QListView::IconMode); - m_grid_view->setResizeMode(QListView::Adjust); - m_grid_view->setUniformItemSizes(true); - m_grid_view->setItemAlignment(Qt::AlignHCenter); - m_grid_view->setContextMenuPolicy(Qt::CustomContextMenu); - m_grid_view->setFrameStyle(QFrame::NoFrame); - m_grid_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); - m_grid_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - m_grid_view->verticalScrollBar()->setSingleStep(15); - - onCoverScaleChanged(); + connect(m_list_view, &QTableView::activated, this, &GameListWidget::onListViewItemActivated); + connect(m_list_view, &QTableView::customContextMenuRequested, this, &GameListWidget::onListViewContextMenuRequested); connect(m_grid_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &GameListWidget::onSelectionModelCurrentChanged); - connect(m_grid_view, &GameListGridListView::zoomIn, this, &GameListWidget::gridZoomIn); - connect(m_grid_view, &GameListGridListView::zoomOut, this, &GameListWidget::gridZoomOut); - connect(m_grid_view, &QListView::activated, this, &GameListWidget::onListViewItemActivated); - connect(m_grid_view, &QListView::customContextMenuRequested, this, &GameListWidget::onListViewContextMenuRequested); - connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged); + connect(m_grid_view, &QListView::activated, this, &GameListWidget::onGridViewItemActivated); + connect(m_grid_view, &QListView::customContextMenuRequested, this, &GameListWidget::onGridViewContextMenuRequested); - m_ui.stack->insertWidget(1, m_grid_view); - - m_empty_widget = new QWidget(m_ui.stack); - m_empty_ui.setupUi(m_empty_widget); - m_empty_ui.supportedFormats->setText(qApp->translate("GameListWidget", SUPPORTED_FORMATS_STRING)); connect(m_empty_ui.addGameDirectory, &QPushButton::clicked, this, [this]() { emit addGameDirectoryRequested(); }); connect(m_empty_ui.scanForNewGames, &QPushButton::clicked, this, [this]() { refresh(false); }); - m_ui.stack->insertWidget(2, m_empty_widget); const bool grid_view = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false); if (grid_view) @@ -1254,7 +1202,7 @@ void GameListWidget::initialize() setFocusProxy(grid_view ? static_cast(m_grid_view) : static_cast(m_list_view)); updateToolbar(); - resizeTableViewColumnsToFit(); + resizeListViewColumnsToFit(); updateBackground(true); } @@ -1401,7 +1349,7 @@ void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, emit selectionChanged(); } -void GameListWidget::onTableViewItemActivated(const QModelIndex& index) +void GameListWidget::onListViewItemActivated(const QModelIndex& index) { const QModelIndex source_index = m_sort_model->mapToSource(index); if (!source_index.isValid() || source_index.row() >= static_cast(GameList::GetEntryCount())) @@ -1420,12 +1368,12 @@ void GameListWidget::onTableViewItemActivated(const QModelIndex& index) } } -void GameListWidget::onTableViewContextMenuRequested(const QPoint& point) +void GameListWidget::onListViewContextMenuRequested(const QPoint& point) { emit entryContextMenuRequested(m_list_view->mapToGlobal(point)); } -void GameListWidget::onListViewItemActivated(const QModelIndex& index) +void GameListWidget::onGridViewItemActivated(const QModelIndex& index) { const QModelIndex source_index = m_sort_model->mapToSource(index); if (!source_index.isValid() || source_index.row() >= static_cast(GameList::GetEntryCount())) @@ -1434,80 +1382,11 @@ void GameListWidget::onListViewItemActivated(const QModelIndex& index) emit entryActivated(); } -void GameListWidget::onListViewContextMenuRequested(const QPoint& point) +void GameListWidget::onGridViewContextMenuRequested(const QPoint& point) { emit entryContextMenuRequested(m_grid_view->mapToGlobal(point)); } -void GameListWidget::onTableViewHeaderContextMenuRequested(const QPoint& point) -{ - QMenu menu; - - for (int column = 0; column < GameListModel::Column_Count; column++) - { - if (column == GameListModel::Column_Cover) - continue; - - QAction* action = menu.addAction(m_model->getColumnDisplayName(column)); - action->setCheckable(true); - action->setChecked(!m_list_view->isColumnHidden(column)); - connect(action, &QAction::toggled, [this, column](bool enabled) { - m_list_view->setColumnHidden(column, !enabled); - saveTableViewColumnVisibilitySettings(column); - resizeTableViewColumnsToFit(); - }); - } - - menu.exec(m_list_view->mapToGlobal(point)); -} - -void GameListWidget::onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder) -{ - saveTableViewColumnSortSettings(); -} - -void GameListWidget::onCoverScaleChanged() -{ - QFont font; - font.setPointSizeF(20.0f * m_model->getCoverScale()); - m_grid_view->setFont(font); - - m_grid_view->updateLayout(); -} - -void GameListWidget::listZoom(float delta) -{ - const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE); - Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); - Host::CommitBaseSettingChanges(); - m_model->setCoverScale(new_scale); - updateToolbar(); - - m_model->refresh(); -} - -void GameListWidget::gridZoomIn() -{ - listZoom(0.05f); -} - -void GameListWidget::gridZoomOut() -{ - listZoom(-0.05f); -} - -void GameListWidget::gridIntScale(int int_scale) -{ - const float new_scale = std::clamp(static_cast(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE); - - Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); - Host::CommitBaseSettingChanges(); - m_model->setCoverScale(new_scale); - updateToolbar(); - - m_model->refresh(); -} - void GameListWidget::refreshGridCovers() { m_model->refreshCovers(); @@ -1545,7 +1424,7 @@ void GameListWidget::showGameList() Host::CommitBaseSettingChanges(); m_ui.stack->setCurrentIndex(0); setFocusProxy(m_list_view); - resizeTableViewColumnsToFit(); + resizeListViewColumnsToFit(); updateToolbar(); emit layoutChanged(); } @@ -1637,126 +1516,22 @@ void GameListWidget::updateToolbar() m_ui.gridScale->setEnabled(grid_view); } +void GameListWidget::onCoverScaleChanged(float scale) +{ + QSignalBlocker sb(m_ui.gridScale); + m_ui.gridScale->setValue(static_cast(scale * 100.0f)); +} + void GameListWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); - resizeTableViewColumnsToFit(); + resizeListViewColumnsToFit(); updateBackground(false); } -void GameListWidget::resizeTableViewColumnsToFit() +void GameListWidget::resizeListViewColumnsToFit() { - QtUtils::ResizeColumnsForTableView(m_list_view, { - 45, // type - 80, // code - -1, // title - -1, // file title - 200, // developer - 200, // publisher - 200, // genre - 50, // year - 100, // players - 85, // time played - 85, // last played - 80, // file size - 80, // size - 55, // region - 100, // achievements - 100 // compatibility - }); -} - -static TinyString getColumnVisibilitySettingsKeyName(int column) -{ - return TinyString::from_format("Show{}", GameListModel::getColumnName(static_cast(column))); -} - -void GameListWidget::loadTableViewColumnVisibilitySettings() -{ - static constexpr std::array DEFAULT_VISIBILITY = {{ - true, // type - true, // code - true, // title - false, // file title - false, // developer - false, // publisher - false, // genre - false, // year - false, // players - true, // time played - true, // last played - true, // file size - false, // size - true, // region - false, // achievements - false // compatibility - }}; - - for (int column = 0; column < GameListModel::Column_Count; column++) - { - const bool visible = Host::GetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), - DEFAULT_VISIBILITY[column]); - m_list_view->setColumnHidden(column, !visible); - } -} - -void GameListWidget::saveTableViewColumnVisibilitySettings() -{ - for (int column = 0; column < GameListModel::Column_Count; column++) - { - const bool visible = !m_list_view->isColumnHidden(column); - Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); - Host::CommitBaseSettingChanges(); - } -} - -void GameListWidget::saveTableViewColumnVisibilitySettings(int column) -{ - const bool visible = !m_list_view->isColumnHidden(column); - Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); - Host::CommitBaseSettingChanges(); -} - -void GameListWidget::loadTableViewColumnSortSettings() -{ - const GameListModel::Column DEFAULT_SORT_COLUMN = GameListModel::Column_Icon; - const bool DEFAULT_SORT_DESCENDING = false; - - const GameListModel::Column sort_column = - GameListModel::getColumnIdForName(Host::GetBaseStringSettingValue("GameListTableView", "SortColumn")) - .value_or(DEFAULT_SORT_COLUMN); - const bool sort_descending = - Host::GetBaseBoolSettingValue("GameListTableView", "SortDescending", DEFAULT_SORT_DESCENDING); - const Qt::SortOrder sort_order = sort_descending ? Qt::DescendingOrder : Qt::AscendingOrder; - m_sort_model->sort(sort_column, sort_order); - if (QHeaderView* hv = m_list_view->horizontalHeader()) - hv->setSortIndicator(sort_column, sort_order); -} - -void GameListWidget::saveTableViewColumnSortSettings() -{ - const int sort_column = m_list_view->horizontalHeader()->sortIndicatorSection(); - const bool sort_descending = (m_list_view->horizontalHeader()->sortIndicatorOrder() == Qt::DescendingOrder); - - if (sort_column >= 0 && sort_column < GameListModel::Column_Count) - { - Host::SetBaseStringSettingValue("GameListTableView", "SortColumn", - GameListModel::getColumnName(static_cast(sort_column))); - } - - Host::SetBaseBoolSettingValue("GameListTableView", "SortDescending", sort_descending); - Host::CommitBaseSettingChanges(); -} - -void GameListWidget::setTableViewColumnHidden(int column, bool hidden) -{ - DebugAssert(column < GameListModel::Column_Count); - if (m_list_view->isColumnHidden(column) == hidden) - return; - - m_list_view->setColumnHidden(column, hidden); - saveTableViewColumnVisibilitySettings(column); - resizeTableViewColumnsToFit(); + m_list_view->resizeColumnsToFit(); } const GameList::Entry* GameListWidget::getSelectedEntry() const @@ -1791,11 +1566,190 @@ const GameList::Entry* GameListWidget::getSelectedEntry() const } } -GameListGridListView::GameListGridListView(GameListModel* model, QWidget* parent) : QListView(parent), m_model(model) +GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent) + : QTableView(parent), m_model(model), m_sort_model(sort_model) { + setModel(sort_model); + setSortingEnabled(true); + setSelectionMode(QAbstractItemView::SingleSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setContextMenuPolicy(Qt::CustomContextMenu); + setAlternatingRowColors(true); + setShowGrid(false); + setCurrentIndex({}); + + QHeaderView* const horizontal_header = horizontalHeader(); + horizontal_header->setHighlightSections(false); + horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu); + + verticalHeader()->hide(); + + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); + + GameListCenterIconStyleDelegate* center_icon_delegate = new GameListCenterIconStyleDelegate(this); + setItemDelegateForColumn(GameListModel::Column_Icon, center_icon_delegate); + setItemDelegateForColumn(GameListModel::Column_Region, center_icon_delegate); + setItemDelegateForColumn(GameListModel::Column_Achievements, + new GameListAchievementsStyleDelegate(this, model, sort_model)); + + loadColumnVisibilitySettings(); + loadColumnSortSettings(); + + connect(horizontal_header, &QHeaderView::sortIndicatorChanged, this, &GameListListView::onHeaderSortIndicatorChanged); + connect(horizontal_header, &QHeaderView::customContextMenuRequested, this, + &GameListListView::onHeaderContextMenuRequested); } -void GameListGridListView::wheelEvent(QWheelEvent* e) +GameListListView::~GameListListView() = default; + +void GameListListView::resizeColumnsToFit() +{ + QtUtils::ResizeColumnsForTableView(this, { + 45, // type + 80, // code + -1, // title + -1, // file title + 200, // developer + 200, // publisher + 200, // genre + 50, // year + 100, // players + 85, // time played + 85, // last played + 80, // file size + 80, // size + 55, // region + 100, // achievements + 100 // compatibility + }); +} + +static TinyString getColumnVisibilitySettingsKeyName(int column) +{ + return TinyString::from_format("Show{}", GameListModel::getColumnName(static_cast(column))); +} + +void GameListListView::loadColumnVisibilitySettings() +{ + static constexpr std::array DEFAULT_VISIBILITY = {{ + true, // type + true, // code + true, // title + false, // file title + false, // developer + false, // publisher + false, // genre + false, // year + false, // players + true, // time played + true, // last played + true, // file size + false, // size + true, // region + false, // achievements + false // compatibility + }}; + + for (int column = 0; column < GameListModel::Column_Count; column++) + { + const bool visible = Host::GetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), + DEFAULT_VISIBILITY[column]); + setColumnHidden(column, !visible); + } +} + +void GameListListView::loadColumnSortSettings() +{ + const GameListModel::Column DEFAULT_SORT_COLUMN = GameListModel::Column_Icon; + const bool DEFAULT_SORT_DESCENDING = false; + + const GameListModel::Column sort_column = + GameListModel::getColumnIdForName(Host::GetBaseStringSettingValue("GameListTableView", "SortColumn")) + .value_or(DEFAULT_SORT_COLUMN); + const bool sort_descending = + Host::GetBaseBoolSettingValue("GameListTableView", "SortDescending", DEFAULT_SORT_DESCENDING); + const Qt::SortOrder sort_order = sort_descending ? Qt::DescendingOrder : Qt::AscendingOrder; + m_sort_model->sort(sort_column, sort_order); + if (QHeaderView* hv = horizontalHeader()) + hv->setSortIndicator(sort_column, sort_order); +} + +void GameListListView::saveColumnSortSettings() +{ + const int sort_column = horizontalHeader()->sortIndicatorSection(); + const bool sort_descending = (horizontalHeader()->sortIndicatorOrder() == Qt::DescendingOrder); + + if (sort_column >= 0 && sort_column < GameListModel::Column_Count) + { + Host::SetBaseStringSettingValue("GameListTableView", "SortColumn", + GameListModel::getColumnName(static_cast(sort_column))); + } + + Host::SetBaseBoolSettingValue("GameListTableView", "SortDescending", sort_descending); + Host::CommitBaseSettingChanges(); +} + +void GameListListView::setAndSaveColumnHidden(int column, bool hidden) +{ + DebugAssert(column < GameListModel::Column_Count); + if (isColumnHidden(column) == hidden) + return; + + setColumnHidden(column, hidden); + Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), !hidden); + Host::CommitBaseSettingChanges(); + resizeColumnsToFit(); +} + +void GameListListView::onHeaderSortIndicatorChanged(int, Qt::SortOrder) +{ + saveColumnSortSettings(); +} + +void GameListListView::onHeaderContextMenuRequested(const QPoint& point) +{ + QMenu menu; + + for (int column = 0; column < GameListModel::Column_Count; column++) + { + if (column == GameListModel::Column_Cover) + continue; + + QAction* action = menu.addAction(m_model->getColumnDisplayName(column)); + action->setCheckable(true); + action->setChecked(!isColumnHidden(column)); + connect(action, &QAction::toggled, [this, column](bool enabled) { + setAndSaveColumnHidden(column, !enabled); + resizeColumnsToFit(); + }); + } + + menu.exec(mapToGlobal(point)); +} + +GameListGridView::GameListGridView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent) + : QListView(parent), m_model(model) +{ + setModel(sort_model); + setModelColumn(GameListModel::Column_Cover); + setSelectionMode(QAbstractItemView::SingleSelection); + setViewMode(QListView::IconMode); + setResizeMode(QListView::Adjust); + setUniformItemSizes(true); + setItemAlignment(Qt::AlignHCenter); + setContextMenuPolicy(Qt::CustomContextMenu); + setFrameStyle(QFrame::NoFrame); + setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); + setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + verticalScrollBar()->setSingleStep(15); + + connect(m_model, &GameListModel::coverScaleChanged, this, &GameListGridView::onCoverScaleChanged); +} + +GameListGridView::~GameListGridView() = default; + +void GameListGridView::wheelEvent(QWheelEvent* e) { if (e->modifiers() & Qt::ControlModifier) { @@ -1814,13 +1768,44 @@ void GameListGridListView::wheelEvent(QWheelEvent* e) QListView::wheelEvent(e); } -void GameListGridListView::resizeEvent(QResizeEvent* e) +void GameListGridView::resizeEvent(QResizeEvent* e) { QListView::resizeEvent(e); updateLayout(); } -void GameListGridListView::updateLayout() +void GameListGridView::onCoverScaleChanged(float scale) +{ + QFont font; + font.setPointSizeF(20.0f * scale); + setFont(font); + + updateLayout(); +} + +void GameListGridView::adjustZoom(float delta) +{ + const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE); + m_model->setCoverScale(new_scale); +} + +void GameListGridView::zoomIn() +{ + adjustZoom(0.05f); +} + +void GameListGridView::zoomOut() +{ + adjustZoom(-0.05f); +} + +void GameListGridView::setZoomPct(int int_scale) +{ + const float new_scale = std::clamp(static_cast(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE); + m_model->setCoverScale(new_scale); +} + +void GameListGridView::updateLayout() { const QScrollBar* const vertical_scrollbar = verticalScrollBar(); const int scrollbar_width = vertical_scrollbar->isVisible() ? vertical_scrollbar->width() : 0; @@ -1848,12 +1833,12 @@ void GameListGridListView::updateLayout() m_vertical_offset = item_spacing; } -int GameListGridListView::horizontalOffset() const +int GameListGridView::horizontalOffset() const { return QListView::horizontalOffset() - m_horizontal_offset; } -int GameListGridListView::verticalOffset() const +int GameListGridView::verticalOffset() const { return QListView::verticalOffset() - m_vertical_offset; } diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index 5bd0dd21c..fa6eaf1de 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -25,7 +25,6 @@ Q_DECLARE_METATYPE(const GameList::Entry*); -class GameListModel; class GameListSortModel; class GameListRefreshThread; @@ -60,7 +59,7 @@ public: static std::optional getColumnIdForName(std::string_view name); static const char* getColumnName(Column col); - GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons, QObject* parent = nullptr); + explicit GameListModel(QObject* parent); ~GameListModel(); int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -100,7 +99,7 @@ public: void updateCacheSize(int num_rows, int num_columns); Q_SIGNALS: - void coverScaleChanged(); + void coverScaleChanged(float scale); private Q_SLOTS: void rowsChanged(const QList& rows); @@ -111,6 +110,7 @@ private: void loadCommonImages(); void loadThemeSpecificImages(); void setColumnDisplayNames(); + void updateCoverScale(); void loadOrGenerateCover(const GameList::Entry* ge); void invalidateCoverForPath(const std::string& path); void coverLoaded(const std::string& path, const QImage& image, float scale); @@ -151,26 +151,58 @@ private: mutable LRUCache m_memcard_pixmap_cache; }; -class GameListGridListView final : public QListView +class GameListListView final : public QTableView { Q_OBJECT public: - GameListGridListView(GameListModel* model, QWidget* parent); + GameListListView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent); + ~GameListListView() override; + + void setAndSaveColumnHidden(int column, bool hidden); + + void resizeColumnsToFit(); + +private Q_SLOTS: + void onHeaderSortIndicatorChanged(int, Qt::SortOrder); + void onHeaderContextMenuRequested(const QPoint& point); + +private: + void loadColumnVisibilitySettings(); + void loadColumnSortSettings(); + void saveColumnSortSettings(); + + GameListModel* m_model = nullptr; + GameListSortModel* m_sort_model = nullptr; +}; + +class GameListGridView final : public QListView +{ + Q_OBJECT + +public: + GameListGridView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent); + ~GameListGridView() override; void updateLayout(); int horizontalOffset() const override; int verticalOffset() const override; -Q_SIGNALS: +public Q_SLOTS: void zoomOut(); void zoomIn(); + void setZoomPct(int int_scale); protected: void wheelEvent(QWheelEvent* e) override; void resizeEvent(QResizeEvent* e) override; +private Q_SLOTS: + void onCoverScaleChanged(float scale); + private: + void adjustZoom(float delta); + GameListModel* m_model = nullptr; int m_horizontal_offset = 0; int m_vertical_offset = 0; @@ -185,10 +217,11 @@ public: ~GameListWidget(); ALWAYS_INLINE GameListModel* getModel() const { return m_model; } + ALWAYS_INLINE GameListListView* getListView() const { return m_list_view; } + ALWAYS_INLINE GameListGridView* getGridView() const { return m_grid_view; } void initialize(); - void resizeTableViewColumnsToFit(); - void setTableViewColumnHidden(int column, bool hidden); + void resizeListViewColumnsToFit(); void refresh(bool invalidate_cache); void refreshModel(); @@ -219,14 +252,13 @@ private Q_SLOTS: void onRefreshProgress(const QString& status, int current, int total, float time); void onRefreshComplete(); + void onCoverScaleChanged(float scale); + void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous); - void onTableViewItemActivated(const QModelIndex& index); - void onTableViewContextMenuRequested(const QPoint& point); - void onTableViewHeaderContextMenuRequested(const QPoint& point); - void onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder); void onListViewItemActivated(const QModelIndex& index); void onListViewContextMenuRequested(const QPoint& point); - void onCoverScaleChanged(); + void onGridViewItemActivated(const QModelIndex& index); + void onGridViewContextMenuRequested(const QPoint& point); void onSearchReturnPressed(); public Q_SLOTS: @@ -235,9 +267,6 @@ public Q_SLOTS: void setShowCoverTitles(bool enabled); void setMergeDiscSets(bool enabled); void setShowGameIcons(bool enabled); - void gridZoomIn(); - void gridZoomOut(); - void gridIntScale(int int_scale); void refreshGridCovers(); void focusSearchWidget(); @@ -245,20 +274,14 @@ protected: void resizeEvent(QResizeEvent* event); private: - void loadTableViewColumnVisibilitySettings(); - void saveTableViewColumnVisibilitySettings(); - void saveTableViewColumnVisibilitySettings(int column); - void loadTableViewColumnSortSettings(); - void saveTableViewColumnSortSettings(); - void listZoom(float delta); void updateToolbar(); Ui::GameListWidget m_ui; GameListModel* m_model = nullptr; GameListSortModel* m_sort_model = nullptr; - QTableView* m_list_view = nullptr; - GameListGridListView* m_grid_view = nullptr; + GameListListView* m_list_view = nullptr; + GameListGridView* m_grid_view = nullptr; QWidget* m_empty_widget = nullptr; Ui::EmptyGameListWidget m_empty_ui; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index c5b2e17dc..17fd8d250 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -449,7 +449,7 @@ void MainWindow::destroyDisplayWidget(bool show_game_list) if (show_game_list) { m_ui.mainContainer->setCurrentIndex(0); - m_game_list_widget->resizeTableViewColumnsToFit(); + m_game_list_widget->resizeListViewColumnsToFit(); } } @@ -1323,13 +1323,13 @@ void MainWindow::onViewSystemDisplayTriggered() void MainWindow::onViewGameGridZoomInActionTriggered() { if (isShowingGameList()) - m_game_list_widget->gridZoomIn(); + m_game_list_widget->getGridView()->zoomIn(); } void MainWindow::onViewGameGridZoomOutActionTriggered() { if (isShowingGameList()) - m_game_list_widget->gridZoomOut(); + m_game_list_widget->getGridView()->zoomOut(); } void MainWindow::onGitHubRepositoryActionTriggered() @@ -2441,7 +2441,7 @@ void MainWindow::showEvent(QShowEvent* event) // gives the incorrect sizes for columns, if you set the style before setting up // the rest of the window... so, instead, let's just force it to be resized on show. if (isShowingGameList()) - m_game_list_widget->resizeTableViewColumnsToFit(); + m_game_list_widget->resizeListViewColumnsToFit(); } void MainWindow::closeEvent(QCloseEvent* event) @@ -2754,7 +2754,7 @@ void MainWindow::onAchievementsLoginSuccess(const QString& username, quint32 poi if (!Host::GetBaseBoolSettingValue("GameListTableView", "TriedShowingAchievementsColumn", false)) { Host::SetBaseBoolSettingValue("GameListTableView", "TriedShowingAchievementsColumn", true); - m_game_list_widget->setTableViewColumnHidden(GameListModel::Column_Achievements, false); + m_game_list_widget->getListView()->setAndSaveColumnHidden(GameListModel::Column_Achievements, false); } }