diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index c2b927c4a..043703fcd 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -15,6 +15,7 @@ #include "common/assert.h" #include "common/file_system.h" +#include "common/log.h" #include "common/path.h" #include "common/string_util.h" @@ -32,6 +33,8 @@ #include #include +LOG_CHANNEL(GameList); + static constexpr float MIN_SCALE = 0.1f; static constexpr float MAX_SCALE = 2.0f; @@ -47,10 +50,10 @@ static constexpr std::array s_column_n {"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played", "Last Played", "Size", "File Size", "Region", "Achievements", "Compatibility", "Cover"}}; -static constexpr int COVER_ART_WIDTH = 512; -static constexpr int COVER_ART_HEIGHT = 512; +static constexpr int COVER_ART_SIZE = 512; static constexpr int COVER_ART_SPACING = 32; static constexpr int MIN_COVER_CACHE_SIZE = 256; +static constexpr int MIN_COVER_CACHE_ROW_BUFFER = 4; static void resizeAndPadImage(QImage* image, int expected_width, int expected_height, bool fill_with_top_left) { @@ -159,11 +162,11 @@ void GameListModel::setCoverScale(float scale) if (loading_image.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath()))) { loading_image.setDevicePixelRatio(dpr); - resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight(), false); + resizeAndPadImage(&loading_image, getCoverArtSize(), getCoverArtSize(), false); } else { - loading_image = QImage(getCoverArtWidth(), getCoverArtHeight(), QImage::Format_RGB32); + loading_image = QImage(getCoverArtSize(), getCoverArtSize(), QImage::Format_RGB32); loading_image.setDevicePixelRatio(dpr); loading_image.fill(QColor(0, 0, 0, 0)); } @@ -173,11 +176,11 @@ void GameListModel::setCoverScale(float scale) if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()))) { m_placeholder_image.setDevicePixelRatio(dpr); - resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight(), false); + resizeAndPadImage(&m_placeholder_image, getCoverArtSize(), getCoverArtSize(), false); } else { - m_placeholder_image = QImage(getCoverArtWidth(), getCoverArtHeight(), QImage::Format_RGB32); + m_placeholder_image = QImage(getCoverArtSize(), getCoverArtSize(), QImage::Format_RGB32); m_placeholder_image.setDevicePixelRatio(dpr); m_placeholder_image.fill(QColor(0, 0, 0, 0)); } @@ -191,14 +194,13 @@ void GameListModel::refreshCovers() refresh(); } -void GameListModel::updateCacheSize(int width, int height) +void GameListModel::updateCacheSize(int num_rows, int num_columns) { + // Add additional buffer zone to the rows, since Qt will grab them early when scrolling. + const int num_items = (num_rows + MIN_COVER_CACHE_ROW_BUFFER) * num_columns; + // This is a bit conversative, since it doesn't consider padding, but better to be over than under. - const int cover_width = getCoverArtWidth(); - const int cover_height = getCoverArtHeight(); - const int num_columns = ((width + (cover_width - 1)) / cover_width); - const int num_rows = ((height + (cover_height - 1)) / cover_height); - m_cover_pixmap_cache.SetMaxCapacity(static_cast(std::max(num_columns * num_rows, MIN_COVER_CACHE_SIZE))); + m_cover_pixmap_cache.SetMaxCapacity(static_cast(std::max(num_items, MIN_COVER_CACHE_SIZE))); } void GameListModel::reloadThemeSpecificImages() @@ -210,8 +212,8 @@ void GameListModel::reloadThemeSpecificImages() void GameListModel::loadOrGenerateCover(const GameList::Entry* ge) { QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, title = ge->title, - placeholder_image = m_placeholder_image, list = this, width = getCoverArtWidth(), - height = getCoverArtHeight(), scale = m_cover_scale, + placeholder_image = m_placeholder_image, list = this, width = getCoverArtSize(), + height = getCoverArtSize(), scale = m_cover_scale, dpr = qApp->devicePixelRatio()]() mutable { QImage image; loadOrGenerateCover(image, placeholder_image, width, height, scale, dpr, path, serial, title); @@ -433,14 +435,9 @@ void GameListModel::fixIconPixmapSize(QPixmap& pm) pm = pm.scaled(new_width, new_height); } -int GameListModel::getCoverArtWidth() const +int GameListModel::getCoverArtSize() 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); + return std::max(static_cast(static_cast(COVER_ART_SIZE) * m_cover_scale), 1); } int GameListModel::getCoverArtSpacing() const @@ -1114,6 +1111,25 @@ 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) @@ -1129,7 +1145,6 @@ void GameListWidget::initialize() 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->updateCacheSize(width(), height()); m_sort_model = new GameListSortModel(m_model); m_sort_model->setSourceModel(m_model); @@ -1165,65 +1180,64 @@ void GameListWidget::initialize() connect(m_ui.searchText, &QLineEdit::returnPressed, this, &GameListWidget::onSearchReturnPressed); GameListCenterIconStyleDelegate* center_icon_delegate = new GameListCenterIconStyleDelegate(this); - m_table_view = new QTableView(m_ui.stack); - m_table_view->setModel(m_sort_model); - m_table_view->setSortingEnabled(true); - m_table_view->setSelectionMode(QAbstractItemView::SingleSelection); - m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); - m_table_view->setContextMenuPolicy(Qt::CustomContextMenu); - m_table_view->setAlternatingRowColors(true); - m_table_view->setShowGrid(false); - m_table_view->setCurrentIndex({}); - m_table_view->horizontalHeader()->setHighlightSections(false); - m_table_view->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); - m_table_view->verticalHeader()->hide(); - m_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - m_table_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); - m_table_view->setItemDelegateForColumn(GameListModel::Column_Icon, center_icon_delegate); - m_table_view->setItemDelegateForColumn(GameListModel::Column_Region, center_icon_delegate); - m_table_view->setItemDelegateForColumn(GameListModel::Column_Achievements, - new GameListAchievementsStyleDelegate(this, m_model, m_sort_model)); + 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_table_view->selectionModel(), &QItemSelectionModel::currentChanged, this, + connect(m_list_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &GameListWidget::onSelectionModelCurrentChanged); - connect(m_table_view, &QTableView::activated, this, &GameListWidget::onTableViewItemActivated); - connect(m_table_view, &QTableView::customContextMenuRequested, this, - &GameListWidget::onTableViewContextMenuRequested); - connect(m_table_view->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, + 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_table_view->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, + connect(m_list_view->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, &GameListWidget::onTableViewHeaderSortIndicatorChanged); - m_ui.stack->insertWidget(0, m_table_view); + m_ui.stack->insertWidget(0, m_list_view); - m_list_view = new GameListGridListView(m_ui.stack); - m_list_view->setModel(m_sort_model); - m_list_view->setModelColumn(GameListModel::Column_Cover); - m_list_view->setSelectionMode(QAbstractItemView::SingleSelection); - m_list_view->setViewMode(QListView::IconMode); - m_list_view->setResizeMode(QListView::Adjust); - m_list_view->setUniformItemSizes(true); - m_list_view->setItemAlignment(Qt::AlignHCenter); - m_list_view->setContextMenuPolicy(Qt::CustomContextMenu); - m_list_view->setFrameStyle(QFrame::NoFrame); - m_list_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel); - m_list_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - m_list_view->verticalScrollBar()->setSingleStep(15); + 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->selectionModel(), &QItemSelectionModel::currentChanged, this, + connect(m_grid_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &GameListWidget::onSelectionModelCurrentChanged); - connect(m_list_view, &GameListGridListView::zoomIn, this, &GameListWidget::gridZoomIn); - connect(m_list_view, &GameListGridListView::zoomOut, this, &GameListWidget::gridZoomOut); - connect(m_list_view, &QListView::activated, this, &GameListWidget::onListViewItemActivated); - connect(m_list_view, &QListView::customContextMenuRequested, this, &GameListWidget::onListViewContextMenuRequested); + 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); - m_ui.stack->insertWidget(1, m_list_view); + m_ui.stack->insertWidget(1, m_grid_view); m_empty_widget = new QWidget(m_ui.stack); m_empty_ui.setupUi(m_empty_widget); @@ -1237,7 +1251,7 @@ void GameListWidget::initialize() m_ui.stack->setCurrentIndex(1); else m_ui.stack->setCurrentIndex(0); - setFocusProxy(grid_view ? static_cast(m_list_view) : static_cast(m_table_view)); + setFocusProxy(grid_view ? static_cast(m_grid_view) : static_cast(m_list_view)); updateToolbar(); resizeTableViewColumnsToFit(); @@ -1321,7 +1335,7 @@ void GameListWidget::updateBackground(bool reload_image) if (m_background_image.isNull()) { m_ui.stack->setPalette(palette()); - m_table_view->setAlternatingRowColors(true); + m_list_view->setAlternatingRowColors(true); return; } @@ -1336,7 +1350,7 @@ void GameListWidget::updateBackground(bool reload_image) QPalette new_palette(m_ui.stack->palette()); new_palette.setBrush(QPalette::Base, QPixmap::fromImage(image)); m_ui.stack->setPalette(new_palette); - m_table_view->setAlternatingRowColors(false); + m_list_view->setAlternatingRowColors(false); }; }); } @@ -1353,7 +1367,7 @@ void GameListWidget::onRefreshProgress(const QString& status, int current, int t { const bool grid_view = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false); m_ui.stack->setCurrentIndex(grid_view ? 1 : 0); - setFocusProxy(grid_view ? static_cast(m_list_view) : static_cast(m_table_view)); + setFocusProxy(grid_view ? static_cast(m_grid_view) : static_cast(m_list_view)); } if (!m_model->hasTakenGameList() || time >= SHORT_REFRESH_TIME) @@ -1408,7 +1422,7 @@ void GameListWidget::onTableViewItemActivated(const QModelIndex& index) void GameListWidget::onTableViewContextMenuRequested(const QPoint& point) { - emit entryContextMenuRequested(m_table_view->mapToGlobal(point)); + emit entryContextMenuRequested(m_list_view->mapToGlobal(point)); } void GameListWidget::onListViewItemActivated(const QModelIndex& index) @@ -1422,7 +1436,7 @@ void GameListWidget::onListViewItemActivated(const QModelIndex& index) void GameListWidget::onListViewContextMenuRequested(const QPoint& point) { - emit entryContextMenuRequested(m_list_view->mapToGlobal(point)); + emit entryContextMenuRequested(m_grid_view->mapToGlobal(point)); } void GameListWidget::onTableViewHeaderContextMenuRequested(const QPoint& point) @@ -1436,15 +1450,15 @@ void GameListWidget::onTableViewHeaderContextMenuRequested(const QPoint& point) QAction* action = menu.addAction(m_model->getColumnDisplayName(column)); action->setCheckable(true); - action->setChecked(!m_table_view->isColumnHidden(column)); + action->setChecked(!m_list_view->isColumnHidden(column)); connect(action, &QAction::toggled, [this, column](bool enabled) { - m_table_view->setColumnHidden(column, !enabled); + m_list_view->setColumnHidden(column, !enabled); saveTableViewColumnVisibilitySettings(column); resizeTableViewColumnsToFit(); }); } - menu.exec(m_table_view->mapToGlobal(point)); + menu.exec(m_list_view->mapToGlobal(point)); } void GameListWidget::onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder) @@ -1454,16 +1468,11 @@ void GameListWidget::onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder) void GameListWidget::onCoverScaleChanged() { - m_model->updateCacheSize(width(), height()); - - m_list_view->setSpacing(m_model->getCoverArtSpacing()); - m_list_view->setIconSize(QSize(m_model->getCoverArtWidth(), m_model->getCoverArtHeight())); - QFont font; font.setPointSizeF(20.0f * m_model->getCoverScale()); - m_list_view->setFont(font); + m_grid_view->setFont(font); - m_list_view->updateLayout(); + m_grid_view->updateLayout(); } void GameListWidget::listZoom(float delta) @@ -1518,7 +1527,7 @@ void GameListWidget::onSearchReturnPressed() return; QAbstractItemView* const target = - isShowingGameGrid() ? static_cast(m_list_view) : static_cast(m_table_view); + isShowingGameGrid() ? static_cast(m_grid_view) : static_cast(m_list_view); target->selectionModel()->select(m_sort_model->index(0, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); target->setFocus(Qt::ShortcutFocusReason); @@ -1535,7 +1544,7 @@ void GameListWidget::showGameList() Host::SetBaseBoolSettingValue("UI", "GameListGridView", false); Host::CommitBaseSettingChanges(); m_ui.stack->setCurrentIndex(0); - setFocusProxy(m_table_view); + setFocusProxy(m_list_view); resizeTableViewColumnsToFit(); updateToolbar(); emit layoutChanged(); @@ -1552,7 +1561,7 @@ void GameListWidget::showGameGrid() Host::SetBaseBoolSettingValue("UI", "GameListGridView", true); Host::CommitBaseSettingChanges(); m_ui.stack->setCurrentIndex(1); - setFocusProxy(m_list_view); + setFocusProxy(m_grid_view); updateToolbar(); emit layoutChanged(); } @@ -1568,6 +1577,7 @@ void GameListWidget::setShowCoverTitles(bool enabled) Host::SetBaseBoolSettingValue("UI", "GameListShowCoverTitles", enabled); Host::CommitBaseSettingChanges(); m_model->setShowCoverTitles(enabled); + m_grid_view->updateLayout(); if (isShowingGameGrid()) m_model->refresh(); updateToolbar(); @@ -1636,24 +1646,24 @@ void GameListWidget::resizeEvent(QResizeEvent* event) void GameListWidget::resizeTableViewColumnsToFit() { - QtUtils::ResizeColumnsForTableView(m_table_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 - }); + 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) @@ -1686,7 +1696,7 @@ void GameListWidget::loadTableViewColumnVisibilitySettings() { const bool visible = Host::GetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), DEFAULT_VISIBILITY[column]); - m_table_view->setColumnHidden(column, !visible); + m_list_view->setColumnHidden(column, !visible); } } @@ -1694,7 +1704,7 @@ void GameListWidget::saveTableViewColumnVisibilitySettings() { for (int column = 0; column < GameListModel::Column_Count; column++) { - const bool visible = !m_table_view->isColumnHidden(column); + const bool visible = !m_list_view->isColumnHidden(column); Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); Host::CommitBaseSettingChanges(); } @@ -1702,7 +1712,7 @@ void GameListWidget::saveTableViewColumnVisibilitySettings() void GameListWidget::saveTableViewColumnVisibilitySettings(int column) { - const bool visible = !m_table_view->isColumnHidden(column); + const bool visible = !m_list_view->isColumnHidden(column); Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); Host::CommitBaseSettingChanges(); } @@ -1719,14 +1729,14 @@ void GameListWidget::loadTableViewColumnSortSettings() 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_table_view->horizontalHeader()) + if (QHeaderView* hv = m_list_view->horizontalHeader()) hv->setSortIndicator(sort_column, sort_order); } void GameListWidget::saveTableViewColumnSortSettings() { - const int sort_column = m_table_view->horizontalHeader()->sortIndicatorSection(); - const bool sort_descending = (m_table_view->horizontalHeader()->sortIndicatorOrder() == Qt::DescendingOrder); + 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) { @@ -1741,10 +1751,10 @@ void GameListWidget::saveTableViewColumnSortSettings() void GameListWidget::setTableViewColumnHidden(int column, bool hidden) { DebugAssert(column < GameListModel::Column_Count); - if (m_table_view->isColumnHidden(column) == hidden) + if (m_list_view->isColumnHidden(column) == hidden) return; - m_table_view->setColumnHidden(column, hidden); + m_list_view->setColumnHidden(column, hidden); saveTableViewColumnVisibilitySettings(column); resizeTableViewColumnsToFit(); } @@ -1753,7 +1763,7 @@ const GameList::Entry* GameListWidget::getSelectedEntry() const { if (m_ui.stack->currentIndex() == 0) { - const QItemSelectionModel* selection_model = m_table_view->selectionModel(); + const QItemSelectionModel* selection_model = m_list_view->selectionModel(); if (!selection_model->hasSelection()) return nullptr; @@ -1769,7 +1779,7 @@ const GameList::Entry* GameListWidget::getSelectedEntry() const } else { - const QItemSelectionModel* selection_model = m_list_view->selectionModel(); + const QItemSelectionModel* selection_model = m_grid_view->selectionModel(); if (!selection_model->hasSelection()) return nullptr; @@ -1781,7 +1791,7 @@ const GameList::Entry* GameListWidget::getSelectedEntry() const } } -GameListGridListView::GameListGridListView(QWidget* parent /*= nullptr*/) : QListView(parent) +GameListGridListView::GameListGridListView(GameListModel* model, QWidget* parent) : QListView(parent), m_model(model) { } @@ -1806,26 +1816,44 @@ void GameListGridListView::wheelEvent(QWheelEvent* e) void GameListGridListView::resizeEvent(QResizeEvent* e) { - updateLayout(); QListView::resizeEvent(e); + updateLayout(); } void GameListGridListView::updateLayout() { - const int scrollbar_width = verticalScrollBar()->width(); - const int item_spacing = spacing(); - const int icon_margin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, this) * 2; + const QScrollBar* const vertical_scrollbar = verticalScrollBar(); + const int scrollbar_width = vertical_scrollbar->isVisible() ? vertical_scrollbar->width() : 0; + const int icon_width = m_model->getCoverArtSize(); + const int item_spacing = m_model->getCoverArtSpacing(); + const int item_margin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, this); + // Split margin+spacing evenly across both sides of each item. // I hate this +2. Not sure what's not being accounted for, but without it the calculation is off by 2 pixels... - const int icon_width = iconSize().width() + icon_margin + item_spacing + 2; - const int available_width = width() - scrollbar_width - item_spacing - icon_margin; - const int items_per_row = available_width / icon_width; - const int margin = (available_width - (items_per_row * icon_width)) / 2; + const int item_width = icon_width + item_margin + item_spacing + 2; + + // one line of text + const int item_height = item_width + (m_model->getShowCoverTitles() ? QFontMetrics(font()).height() : 0); + + const int available_width = width() - scrollbar_width; + const int num_columns = available_width / item_width; + const int num_rows = (height() + (item_height - 1)) / item_height; + const int margin = (available_width - (num_columns * item_width)) / 2; + + setGridSize(QSize(item_width, item_height)); + + m_model->updateCacheSize(num_rows, num_columns); m_horizontal_offset = margin; + m_vertical_offset = item_spacing; } int GameListGridListView::horizontalOffset() const { return QListView::horizontalOffset() - m_horizontal_offset; } + +int GameListGridListView::verticalOffset() const +{ + return QListView::verticalOffset() - m_vertical_offset; +} diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index d52ab4df0..5bd0dd21c 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -94,11 +94,10 @@ public: float getCoverScale() const { return m_cover_scale; } void setCoverScale(float scale); - int getCoverArtWidth() const; - int getCoverArtHeight() const; + int getCoverArtSize() const; int getCoverArtSpacing() const; void refreshCovers(); - void updateCacheSize(int width, int height); + void updateCacheSize(int num_rows, int num_columns); Q_SIGNALS: void coverScaleChanged(); @@ -157,10 +156,11 @@ class GameListGridListView final : public QListView Q_OBJECT public: - GameListGridListView(QWidget* parent = nullptr); + GameListGridListView(GameListModel* model, QWidget* parent); void updateLayout(); int horizontalOffset() const override; + int verticalOffset() const override; Q_SIGNALS: void zoomOut(); @@ -171,7 +171,9 @@ protected: void resizeEvent(QResizeEvent* e) override; private: + GameListModel* m_model = nullptr; int m_horizontal_offset = 0; + int m_vertical_offset = 0; }; class GameListWidget final : public QWidget @@ -255,8 +257,8 @@ private: GameListModel* m_model = nullptr; GameListSortModel* m_sort_model = nullptr; - QTableView* m_table_view = nullptr; - GameListGridListView* m_list_view = nullptr; + QTableView* m_list_view = nullptr; + GameListGridListView* m_grid_view = nullptr; QWidget* m_empty_widget = nullptr; Ui::EmptyGameListWidget m_empty_ui;