Skip to content

Commit b0dd909

Browse files
authored
Qt: Resizable game list icons (#3539)
* Qt: Sharp Bilinear scaling for gamelist icons * Single function for Sharp Bilinear scaling of icons * Qt: Resizable game list icons [PoC] * Fixed dynamic row scaling and size slider * fix some duplicate lines * made scaleMemoryCardIconWithSharpBilinear inline and added constant for icon padding * removed resizeEvent from GameListListView
1 parent 56e1713 commit b0dd909

File tree

5 files changed

+172
-28
lines changed

5 files changed

+172
-28
lines changed

src/duckstation-qt/gamelistwidget.cpp

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838

3939
LOG_CHANNEL(GameList);
4040

41-
static constexpr float MIN_SCALE = 0.1f;
42-
static constexpr float MAX_SCALE = 2.0f;
41+
static constexpr float MIN_ICON_SCALE = 1.0f;
42+
static constexpr float MAX_ICON_SCALE = 5.0f;
43+
static constexpr float MIN_COVER_SCALE = 0.1f;
44+
static constexpr float MAX_COVER_SCALE = 2.0f;
4345

4446
static const char* SUPPORTED_FORMATS_STRING =
4547
QT_TRANSLATE_NOOP(GameListWidget, ".cue (Cue Sheets)\n"
@@ -57,6 +59,8 @@ static constexpr int COVER_ART_SIZE = 512;
5759
static constexpr int COVER_ART_SPACING = 32;
5860
static constexpr int MIN_COVER_CACHE_SIZE = 256;
5961
static constexpr int MIN_COVER_CACHE_ROW_BUFFER = 4;
62+
static constexpr int MEMORY_CARD_ICON_SIZE = 16;
63+
static constexpr int MEMORY_CARD_ICON_PADDING = 12;
6064

6165
static void resizeAndPadImage(QImage* image, int expected_width, int expected_height, bool fill_with_top_left)
6266
{
@@ -127,6 +131,7 @@ GameListModel::GameListModel(QObject* parent)
127131
: QAbstractTableModel(parent), m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE)
128132
{
129133
m_cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
134+
m_icon_scale = Host::GetBaseFloatSettingValue("UI", "GameListIconScale", 1.00f);
130135
m_show_localized_titles = GameList::ShouldShowLocalizedTitles();
131136
m_show_titles_for_covers = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
132137
m_show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true);
@@ -174,6 +179,26 @@ void GameListModel::refreshIcons()
174179
emit dataChanged(index(0, Column_Icon), index(rowCount() - 1, Column_Icon), {Qt::DecorationRole});
175180
}
176181

182+
void GameListModel::setIconScale(float scale)
183+
{
184+
if (m_icon_scale == scale)
185+
return;
186+
187+
m_icon_scale = scale;
188+
189+
Host::SetBaseFloatSettingValue("UI", "GameListIconScale", scale);
190+
Host::CommitBaseSettingChanges();
191+
updateIconScale();
192+
}
193+
194+
void GameListModel::updateIconScale()
195+
{
196+
m_memcard_pixmap_cache.Clear();
197+
198+
emit iconScaleChanged(m_icon_scale);
199+
refresh();
200+
}
201+
177202
void GameListModel::setCoverScale(float scale)
178203
{
179204
if (m_cover_scale == scale)
@@ -393,7 +418,7 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
393418
QPixmap pm;
394419
if (!path.empty() && pm.load(QString::fromStdString(path)))
395420
{
396-
fixIconPixmapSize(pm);
421+
const_cast<GameListModel*>(this)->fixIconPixmapSize(pm);
397422
return *m_memcard_pixmap_cache.Insert(ge->serial, std::move(pm));
398423
}
399424

@@ -467,16 +492,16 @@ void GameListModel::fixIconPixmapSize(QPixmap& pm)
467492
const int width = static_cast<int>(static_cast<float>(pm.width()) * dpr);
468493
const int height = static_cast<int>(static_cast<float>(pm.height()) * dpr);
469494
const int max_dim = std::max(width, height);
470-
if (max_dim == 16)
471-
return;
472495

473496
const float wanted_dpr = qApp->devicePixelRatio();
474497
pm.setDevicePixelRatio(wanted_dpr);
475498

476-
const float scale = static_cast<float>(max_dim) / 16.0f / wanted_dpr;
499+
const float scale = static_cast<float>(max_dim) / MEMORY_CARD_ICON_SIZE / wanted_dpr / m_icon_scale;
477500
const int new_width = static_cast<int>(static_cast<float>(width) / scale);
478501
const int new_height = static_cast<int>(static_cast<float>(height) / scale);
479-
pm = pm.scaled(new_width, new_height);
502+
503+
if (width != new_width || height != new_height)
504+
QtUtils::scaleMemoryCardIconWithSharpBilinear(pm, std::max(new_width, new_height));
480505
}
481506

482507
int GameListModel::getCoverArtSize() const
@@ -1248,6 +1273,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
12481273
{
12491274
m_model = new GameListModel(this);
12501275
connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged);
1276+
connect(m_model, &GameListModel::iconScaleChanged, this, &GameListWidget::onIconScaleChanged);
12511277

12521278
m_sort_model = new GameListSortModel(m_model);
12531279
m_sort_model->setSourceModel(m_model);
@@ -1284,6 +1310,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
12841310
m_ui.showLocalizedTitles->setDefaultAction(actionShowLocalizedTitles);
12851311

12861312
connect(m_ui.gridScale, &QSlider::valueChanged, m_grid_view, &GameListGridView::setZoomPct);
1313+
connect(m_ui.listScale, &QSlider::valueChanged, m_list_view, &GameListListView::setZoomPct);
12871314
connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
12881315
m_sort_model->setFilterType((index == 0) ? GameList::EntryType::MaxCount :
12891316
static_cast<GameList::EntryType>(index - 1));
@@ -1318,6 +1345,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
13181345
actionListShowIcons->setChecked(m_model->getShowGameIcons());
13191346
actionGridShowTitles->setChecked(m_model->getShowCoverTitles());
13201347
onCoverScaleChanged(m_model->getCoverScale());
1348+
onIconScaleChanged(m_model->getIconScale());
13211349

13221350
updateView(grid_view);
13231351
updateToolbar(grid_view);
@@ -1623,6 +1651,7 @@ void GameListWidget::updateToolbar(bool grid_view)
16231651
m_ui.showGameIcons->setVisible(!grid_view);
16241652
m_ui.showGridTitles->setVisible(grid_view);
16251653
m_ui.gridScale->setVisible(grid_view);
1654+
m_ui.listScale->setVisible(!grid_view);
16261655
}
16271656

16281657
void GameListWidget::onCoverScaleChanged(float scale)
@@ -1631,6 +1660,12 @@ void GameListWidget::onCoverScaleChanged(float scale)
16311660
m_ui.gridScale->setValue(static_cast<int>(scale * 100.0f));
16321661
}
16331662

1663+
void GameListWidget::onIconScaleChanged(float scale)
1664+
{
1665+
QSignalBlocker sb(m_ui.listScale);
1666+
m_ui.listScale->setValue(static_cast<int>(scale * 4.0f));
1667+
}
1668+
16341669
void GameListWidget::resizeEvent(QResizeEvent* event)
16351670
{
16361671
QWidget::resizeEvent(event);
@@ -1688,8 +1723,12 @@ GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort
16881723

16891724
horizontal_header->setSectionResizeMode(GameListModel::Column_Title, QHeaderView::Stretch);
16901725
horizontal_header->setSectionResizeMode(GameListModel::Column_FileTitle, QHeaderView::Stretch);
1726+
horizontal_header->setSectionResizeMode(GameListModel::Column_Icon, QHeaderView::ResizeToContents);
16911727

1692-
verticalHeader()->hide();
1728+
QHeaderView* const vertical_header = verticalHeader();
1729+
vertical_header->hide();
1730+
vertical_header->setDefaultSectionSize(MEMORY_CARD_ICON_SIZE + MEMORY_CARD_ICON_PADDING +
1731+
style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, this));
16931732

16941733
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
16951734
setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
@@ -1706,10 +1745,30 @@ GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort
17061745
connect(horizontal_header, &QHeaderView::sortIndicatorChanged, this, &GameListListView::onHeaderSortIndicatorChanged);
17071746
connect(horizontal_header, &QHeaderView::customContextMenuRequested, this,
17081747
&GameListListView::onHeaderContextMenuRequested);
1748+
connect(m_model, &GameListModel::iconScaleChanged, this, &GameListListView::onIconScaleChanged);
17091749
}
17101750

17111751
GameListListView::~GameListListView() = default;
17121752

1753+
void GameListListView::wheelEvent(QWheelEvent* e)
1754+
{
1755+
if (e->modifiers() & Qt::ControlModifier)
1756+
{
1757+
int dy = e->angleDelta().y();
1758+
if (dy != 0)
1759+
{
1760+
if (dy < 0)
1761+
zoomOut();
1762+
else
1763+
zoomIn();
1764+
1765+
return;
1766+
}
1767+
}
1768+
1769+
QTableView::wheelEvent(e);
1770+
}
1771+
17131772
void GameListListView::setFixedColumnWidth(int column, int width)
17141773
{
17151774
horizontalHeader()->setSectionResizeMode(column, QHeaderView::Fixed);
@@ -1865,6 +1924,44 @@ void GameListListView::onHeaderContextMenuRequested(const QPoint& point)
18651924
menu.exec(mapToGlobal(point));
18661925
}
18671926

1927+
void GameListListView::onIconScaleChanged(float scale)
1928+
{
1929+
updateLayout();
1930+
}
1931+
1932+
void GameListListView::adjustZoom(float delta)
1933+
{
1934+
const float new_scale = std::clamp(m_model->getIconScale() + delta, MIN_ICON_SCALE, MAX_ICON_SCALE);
1935+
m_model->setIconScale(new_scale);
1936+
}
1937+
1938+
void GameListListView::zoomIn()
1939+
{
1940+
adjustZoom(0.25f);
1941+
}
1942+
1943+
void GameListListView::zoomOut()
1944+
{
1945+
adjustZoom(-0.25f);
1946+
}
1947+
1948+
void GameListListView::setZoomPct(int int_scale)
1949+
{
1950+
const float new_scale = std::clamp(static_cast<float>(int_scale) / 4.0f, MIN_ICON_SCALE, MAX_ICON_SCALE);
1951+
m_model->setIconScale(new_scale);
1952+
}
1953+
1954+
void GameListListView::updateLayout()
1955+
{
1956+
const float row_count = m_model->rowCount();
1957+
const float icon_scale = m_model->getIconScale();
1958+
const int height =
1959+
icon_scale * MEMORY_CARD_ICON_SIZE + 12 + style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, this);
1960+
1961+
for (int i = 0; i < row_count; i++)
1962+
setRowHeight(i, height);
1963+
}
1964+
18681965
GameListGridView::GameListGridView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent)
18691966
: QListView(parent), m_model(model)
18701967
{
@@ -1922,7 +2019,7 @@ void GameListGridView::onCoverScaleChanged(float scale)
19222019

19232020
void GameListGridView::adjustZoom(float delta)
19242021
{
1925-
const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE);
2022+
const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_COVER_SCALE, MAX_COVER_SCALE);
19262023
m_model->setCoverScale(new_scale);
19272024
}
19282025

@@ -1938,7 +2035,7 @@ void GameListGridView::zoomOut()
19382035

19392036
void GameListGridView::setZoomPct(int int_scale)
19402037
{
1941-
const float new_scale = std::clamp(static_cast<float>(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE);
2038+
const float new_scale = std::clamp(static_cast<float>(int_scale) / 100.0f, MIN_COVER_SCALE, MAX_COVER_SCALE);
19422039
m_model->setCoverScale(new_scale);
19432040
}
19442041

src/duckstation-qt/gamelistwidget.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class GameListModel final : public QAbstractTableModel
9393
bool getShowCoverTitles() const { return m_show_titles_for_covers; }
9494
void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; }
9595

96+
float getIconScale() const { return m_icon_scale; }
97+
void setIconScale(float scale);
9698
bool getShowGameIcons() const { return m_show_game_icons; }
9799
void setShowGameIcons(bool enabled);
98100
QIcon getIconForGame(const QString& path);
@@ -107,6 +109,7 @@ class GameListModel final : public QAbstractTableModel
107109

108110
Q_SIGNALS:
109111
void coverScaleChanged(float scale);
112+
void iconScaleChanged(float scale);
110113

111114
private:
112115
void rowsChanged(const QList<int>& rows);
@@ -116,6 +119,7 @@ class GameListModel final : public QAbstractTableModel
116119
void loadThemeSpecificImages();
117120
void setColumnDisplayNames();
118121
void updateCoverScale();
122+
void updateIconScale();
119123
void loadOrGenerateCover(const GameList::Entry* ge);
120124
void invalidateCoverForPath(const std::string& path);
121125
void coverLoaded(const std::string& path, const QImage& image, float scale);
@@ -128,10 +132,12 @@ class GameListModel final : public QAbstractTableModel
128132

129133
const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const;
130134
const QPixmap& getFlagPixmapForEntry(const GameList::Entry* ge) const;
131-
static void fixIconPixmapSize(QPixmap& pm);
135+
void fixIconPixmapSize(QPixmap& pm);
136+
132137
std::optional<GameList::EntryList> m_taken_entries;
133138

134139
float m_cover_scale = 0.0f;
140+
float m_icon_scale = 0.0f;
135141
bool m_show_localized_titles = false;
136142
bool m_show_titles_for_covers = false;
137143
bool m_show_game_icons = false;
@@ -163,8 +169,20 @@ class GameListListView final : public QTableView
163169
~GameListListView() override;
164170

165171
void setAndSaveColumnHidden(int column, bool hidden);
172+
void updateLayout();
173+
174+
public Q_SLOTS:
175+
void zoomOut();
176+
void zoomIn();
177+
void setZoomPct(int int_scale);
178+
179+
protected:
180+
void wheelEvent(QWheelEvent* e) override;
166181

167182
private:
183+
void onIconScaleChanged(float scale);
184+
void adjustZoom(float delta);
185+
168186
void onHeaderSortIndicatorChanged(int, Qt::SortOrder);
169187
void onHeaderContextMenuRequested(const QPoint& point);
170188

@@ -250,6 +268,7 @@ private Q_SLOTS:
250268
void onRefreshComplete();
251269

252270
void onCoverScaleChanged(float scale);
271+
void onIconScaleChanged(float scale);
253272

254273
void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
255274
void onListViewItemActivated(const QModelIndex& index);

src/duckstation-qt/gamelistwidget.ui

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>821</width>
9+
<width>1063</width>
1010
<height>619</height>
1111
</rect>
1212
</property>
@@ -182,6 +182,31 @@
182182
</property>
183183
</widget>
184184
</item>
185+
<item>
186+
<widget class="QSlider" name="listScale">
187+
<property name="minimumSize">
188+
<size>
189+
<width>125</width>
190+
<height>0</height>
191+
</size>
192+
</property>
193+
<property name="maximumSize">
194+
<size>
195+
<width>125</width>
196+
<height>16777215</height>
197+
</size>
198+
</property>
199+
<property name="minimum">
200+
<number>4</number>
201+
</property>
202+
<property name="maximum">
203+
<number>20</number>
204+
</property>
205+
<property name="orientation">
206+
<enum>Qt::Orientation::Horizontal</enum>
207+
</property>
208+
</widget>
209+
</item>
185210
<item>
186211
<spacer name="horizontalSpacer">
187212
<property name="orientation">

src/duckstation-qt/memorycardeditorwindow.cpp

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ class MemoryCardEditorIconStyleDelegate final : public QStyledItemDelegate
8686

8787
// doing this on the UI thread is a bit ehh, but whatever, they're small images.
8888
const MemoryCardImage::IconFrame& frame = fi.icon_frames[real_frame_index];
89-
const int pixmap_width = static_cast<int>(std::ceil(static_cast<qreal>(rc.width()) * dpr));
90-
const int pixmap_height = static_cast<int>(std::ceil(static_cast<qreal>(rc.height()) * dpr));
89+
const int pixmap_width = static_cast<int>(std::ceil(static_cast<qreal>(rc.width() - 1) * dpr));
90+
const int pixmap_height = static_cast<int>(std::ceil(static_cast<qreal>(rc.height() - 1) * dpr));
9191
const int icon_size = std::min(pixmap_width, pixmap_height);
9292
const int xoffs =
9393
std::max(static_cast<int>((static_cast<qreal>(pixmap_width - icon_size) * static_cast<qreal>(0.5)) / dpr), 0);
@@ -97,20 +97,7 @@ class MemoryCardEditorIconStyleDelegate final : public QStyledItemDelegate
9797
QImage src_image = QImage(reinterpret_cast<const uchar*>(frame.pixels), MemoryCardImage::ICON_WIDTH,
9898
MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
9999
if (src_image.width() != icon_size || src_image.height() != icon_size)
100-
{
101-
// Sharp Bilinear scaling
102-
// First, scale the icon by the largest integer size using nearest-neighbor...
103-
const float scaled_icon_size = MEMORY_CARD_ICON_SIZE * dpr;
104-
const int integer_icon_size =
105-
static_cast<int>(scaled_icon_size / static_cast<float>(MemoryCardImage::ICON_HEIGHT)) *
106-
static_cast<int>(MemoryCardImage::ICON_HEIGHT);
107-
src_image =
108-
src_image.scaled(integer_icon_size, integer_icon_size, Qt::IgnoreAspectRatio, Qt::FastTransformation);
109-
110-
// ...then scale any remainder using bilinear interpolation.
111-
if (scaled_icon_size - integer_icon_size > 0)
112-
src_image = src_image.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
113-
}
100+
QtUtils::scaleMemoryCardIconWithSharpBilinear(src_image, icon_size);
114101

115102
src_image.setDevicePixelRatio(dpr);
116103

0 commit comments

Comments
 (0)