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
pull/3541/head
Ariel Nogueira Kovaljski 2 months ago committed by GitHub
parent 56e1713e27
commit b0dd909cf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -38,8 +38,10 @@
LOG_CHANNEL(GameList);
static constexpr float MIN_SCALE = 0.1f;
static constexpr float MAX_SCALE = 2.0f;
static constexpr float MIN_ICON_SCALE = 1.0f;
static constexpr float MAX_ICON_SCALE = 5.0f;
static constexpr float MIN_COVER_SCALE = 0.1f;
static constexpr float MAX_COVER_SCALE = 2.0f;
static const char* SUPPORTED_FORMATS_STRING =
QT_TRANSLATE_NOOP(GameListWidget, ".cue (Cue Sheets)\n"
@ -57,6 +59,8 @@ 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 constexpr int MEMORY_CARD_ICON_SIZE = 16;
static constexpr int MEMORY_CARD_ICON_PADDING = 12;
static void resizeAndPadImage(QImage* image, int expected_width, int expected_height, bool fill_with_top_left)
{
@ -127,6 +131,7 @@ GameListModel::GameListModel(QObject* parent)
: QAbstractTableModel(parent), m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE)
{
m_cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
m_icon_scale = Host::GetBaseFloatSettingValue("UI", "GameListIconScale", 1.00f);
m_show_localized_titles = GameList::ShouldShowLocalizedTitles();
m_show_titles_for_covers = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
m_show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true);
@ -174,6 +179,26 @@ void GameListModel::refreshIcons()
emit dataChanged(index(0, Column_Icon), index(rowCount() - 1, Column_Icon), {Qt::DecorationRole});
}
void GameListModel::setIconScale(float scale)
{
if (m_icon_scale == scale)
return;
m_icon_scale = scale;
Host::SetBaseFloatSettingValue("UI", "GameListIconScale", scale);
Host::CommitBaseSettingChanges();
updateIconScale();
}
void GameListModel::updateIconScale()
{
m_memcard_pixmap_cache.Clear();
emit iconScaleChanged(m_icon_scale);
refresh();
}
void GameListModel::setCoverScale(float scale)
{
if (m_cover_scale == scale)
@ -393,7 +418,7 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
QPixmap pm;
if (!path.empty() && pm.load(QString::fromStdString(path)))
{
fixIconPixmapSize(pm);
const_cast<GameListModel*>(this)->fixIconPixmapSize(pm);
return *m_memcard_pixmap_cache.Insert(ge->serial, std::move(pm));
}
@ -467,16 +492,16 @@ void GameListModel::fixIconPixmapSize(QPixmap& pm)
const int width = static_cast<int>(static_cast<float>(pm.width()) * dpr);
const int height = static_cast<int>(static_cast<float>(pm.height()) * dpr);
const int max_dim = std::max(width, height);
if (max_dim == 16)
return;
const float wanted_dpr = qApp->devicePixelRatio();
pm.setDevicePixelRatio(wanted_dpr);
const float scale = static_cast<float>(max_dim) / 16.0f / wanted_dpr;
const float scale = static_cast<float>(max_dim) / MEMORY_CARD_ICON_SIZE / wanted_dpr / m_icon_scale;
const int new_width = static_cast<int>(static_cast<float>(width) / scale);
const int new_height = static_cast<int>(static_cast<float>(height) / scale);
pm = pm.scaled(new_width, new_height);
if (width != new_width || height != new_height)
QtUtils::scaleMemoryCardIconWithSharpBilinear(pm, std::max(new_width, new_height));
}
int GameListModel::getCoverArtSize() const
@ -1248,6 +1273,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
{
m_model = new GameListModel(this);
connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged);
connect(m_model, &GameListModel::iconScaleChanged, this, &GameListWidget::onIconScaleChanged);
m_sort_model = new GameListSortModel(m_model);
m_sort_model->setSourceModel(m_model);
@ -1284,6 +1310,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
m_ui.showLocalizedTitles->setDefaultAction(actionShowLocalizedTitles);
connect(m_ui.gridScale, &QSlider::valueChanged, m_grid_view, &GameListGridView::setZoomPct);
connect(m_ui.listScale, &QSlider::valueChanged, m_list_view, &GameListListView::setZoomPct);
connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
m_sort_model->setFilterType((index == 0) ? GameList::EntryType::MaxCount :
static_cast<GameList::EntryType>(index - 1));
@ -1318,6 +1345,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
actionListShowIcons->setChecked(m_model->getShowGameIcons());
actionGridShowTitles->setChecked(m_model->getShowCoverTitles());
onCoverScaleChanged(m_model->getCoverScale());
onIconScaleChanged(m_model->getIconScale());
updateView(grid_view);
updateToolbar(grid_view);
@ -1623,6 +1651,7 @@ void GameListWidget::updateToolbar(bool grid_view)
m_ui.showGameIcons->setVisible(!grid_view);
m_ui.showGridTitles->setVisible(grid_view);
m_ui.gridScale->setVisible(grid_view);
m_ui.listScale->setVisible(!grid_view);
}
void GameListWidget::onCoverScaleChanged(float scale)
@ -1631,6 +1660,12 @@ void GameListWidget::onCoverScaleChanged(float scale)
m_ui.gridScale->setValue(static_cast<int>(scale * 100.0f));
}
void GameListWidget::onIconScaleChanged(float scale)
{
QSignalBlocker sb(m_ui.listScale);
m_ui.listScale->setValue(static_cast<int>(scale * 4.0f));
}
void GameListWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
@ -1688,8 +1723,12 @@ GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort
horizontal_header->setSectionResizeMode(GameListModel::Column_Title, QHeaderView::Stretch);
horizontal_header->setSectionResizeMode(GameListModel::Column_FileTitle, QHeaderView::Stretch);
horizontal_header->setSectionResizeMode(GameListModel::Column_Icon, QHeaderView::ResizeToContents);
verticalHeader()->hide();
QHeaderView* const vertical_header = verticalHeader();
vertical_header->hide();
vertical_header->setDefaultSectionSize(MEMORY_CARD_ICON_SIZE + MEMORY_CARD_ICON_PADDING +
style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, this));
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
@ -1706,10 +1745,30 @@ GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort
connect(horizontal_header, &QHeaderView::sortIndicatorChanged, this, &GameListListView::onHeaderSortIndicatorChanged);
connect(horizontal_header, &QHeaderView::customContextMenuRequested, this,
&GameListListView::onHeaderContextMenuRequested);
connect(m_model, &GameListModel::iconScaleChanged, this, &GameListListView::onIconScaleChanged);
}
GameListListView::~GameListListView() = default;
void GameListListView::wheelEvent(QWheelEvent* e)
{
if (e->modifiers() & Qt::ControlModifier)
{
int dy = e->angleDelta().y();
if (dy != 0)
{
if (dy < 0)
zoomOut();
else
zoomIn();
return;
}
}
QTableView::wheelEvent(e);
}
void GameListListView::setFixedColumnWidth(int column, int width)
{
horizontalHeader()->setSectionResizeMode(column, QHeaderView::Fixed);
@ -1865,6 +1924,44 @@ void GameListListView::onHeaderContextMenuRequested(const QPoint& point)
menu.exec(mapToGlobal(point));
}
void GameListListView::onIconScaleChanged(float scale)
{
updateLayout();
}
void GameListListView::adjustZoom(float delta)
{
const float new_scale = std::clamp(m_model->getIconScale() + delta, MIN_ICON_SCALE, MAX_ICON_SCALE);
m_model->setIconScale(new_scale);
}
void GameListListView::zoomIn()
{
adjustZoom(0.25f);
}
void GameListListView::zoomOut()
{
adjustZoom(-0.25f);
}
void GameListListView::setZoomPct(int int_scale)
{
const float new_scale = std::clamp(static_cast<float>(int_scale) / 4.0f, MIN_ICON_SCALE, MAX_ICON_SCALE);
m_model->setIconScale(new_scale);
}
void GameListListView::updateLayout()
{
const float row_count = m_model->rowCount();
const float icon_scale = m_model->getIconScale();
const int height =
icon_scale * MEMORY_CARD_ICON_SIZE + 12 + style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, this);
for (int i = 0; i < row_count; i++)
setRowHeight(i, height);
}
GameListGridView::GameListGridView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent)
: QListView(parent), m_model(model)
{
@ -1922,7 +2019,7 @@ void GameListGridView::onCoverScaleChanged(float scale)
void GameListGridView::adjustZoom(float delta)
{
const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE);
const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_COVER_SCALE, MAX_COVER_SCALE);
m_model->setCoverScale(new_scale);
}
@ -1938,7 +2035,7 @@ void GameListGridView::zoomOut()
void GameListGridView::setZoomPct(int int_scale)
{
const float new_scale = std::clamp(static_cast<float>(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE);
const float new_scale = std::clamp(static_cast<float>(int_scale) / 100.0f, MIN_COVER_SCALE, MAX_COVER_SCALE);
m_model->setCoverScale(new_scale);
}

@ -93,6 +93,8 @@ public:
bool getShowCoverTitles() const { return m_show_titles_for_covers; }
void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; }
float getIconScale() const { return m_icon_scale; }
void setIconScale(float scale);
bool getShowGameIcons() const { return m_show_game_icons; }
void setShowGameIcons(bool enabled);
QIcon getIconForGame(const QString& path);
@ -107,6 +109,7 @@ public:
Q_SIGNALS:
void coverScaleChanged(float scale);
void iconScaleChanged(float scale);
private:
void rowsChanged(const QList<int>& rows);
@ -116,6 +119,7 @@ private:
void loadThemeSpecificImages();
void setColumnDisplayNames();
void updateCoverScale();
void updateIconScale();
void loadOrGenerateCover(const GameList::Entry* ge);
void invalidateCoverForPath(const std::string& path);
void coverLoaded(const std::string& path, const QImage& image, float scale);
@ -128,10 +132,12 @@ private:
const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const;
const QPixmap& getFlagPixmapForEntry(const GameList::Entry* ge) const;
static void fixIconPixmapSize(QPixmap& pm);
void fixIconPixmapSize(QPixmap& pm);
std::optional<GameList::EntryList> m_taken_entries;
float m_cover_scale = 0.0f;
float m_icon_scale = 0.0f;
bool m_show_localized_titles = false;
bool m_show_titles_for_covers = false;
bool m_show_game_icons = false;
@ -163,8 +169,20 @@ public:
~GameListListView() override;
void setAndSaveColumnHidden(int column, bool hidden);
void updateLayout();
public Q_SLOTS:
void zoomOut();
void zoomIn();
void setZoomPct(int int_scale);
protected:
void wheelEvent(QWheelEvent* e) override;
private:
void onIconScaleChanged(float scale);
void adjustZoom(float delta);
void onHeaderSortIndicatorChanged(int, Qt::SortOrder);
void onHeaderContextMenuRequested(const QPoint& point);
@ -250,6 +268,7 @@ private Q_SLOTS:
void onRefreshComplete();
void onCoverScaleChanged(float scale);
void onIconScaleChanged(float scale);
void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
void onListViewItemActivated(const QModelIndex& index);

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>821</width>
<width>1063</width>
<height>619</height>
</rect>
</property>
@ -182,6 +182,31 @@
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="listScale">
<property name="minimumSize">
<size>
<width>125</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>125</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>4</number>
</property>
<property name="maximum">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">

@ -86,8 +86,8 @@ public:
// doing this on the UI thread is a bit ehh, but whatever, they're small images.
const MemoryCardImage::IconFrame& frame = fi.icon_frames[real_frame_index];
const int pixmap_width = static_cast<int>(std::ceil(static_cast<qreal>(rc.width()) * dpr));
const int pixmap_height = static_cast<int>(std::ceil(static_cast<qreal>(rc.height()) * dpr));
const int pixmap_width = static_cast<int>(std::ceil(static_cast<qreal>(rc.width() - 1) * dpr));
const int pixmap_height = static_cast<int>(std::ceil(static_cast<qreal>(rc.height() - 1) * dpr));
const int icon_size = std::min(pixmap_width, pixmap_height);
const int xoffs =
std::max(static_cast<int>((static_cast<qreal>(pixmap_width - icon_size) * static_cast<qreal>(0.5)) / dpr), 0);
@ -97,20 +97,7 @@ public:
QImage src_image = QImage(reinterpret_cast<const uchar*>(frame.pixels), MemoryCardImage::ICON_WIDTH,
MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
if (src_image.width() != icon_size || src_image.height() != icon_size)
{
// Sharp Bilinear scaling
// First, scale the icon by the largest integer size using nearest-neighbor...
const float scaled_icon_size = MEMORY_CARD_ICON_SIZE * dpr;
const int integer_icon_size =
static_cast<int>(scaled_icon_size / static_cast<float>(MemoryCardImage::ICON_HEIGHT)) *
static_cast<int>(MemoryCardImage::ICON_HEIGHT);
src_image =
src_image.scaled(integer_icon_size, integer_icon_size, Qt::IgnoreAspectRatio, Qt::FastTransformation);
// ...then scale any remainder using bilinear interpolation.
if (scaled_icon_size - integer_icon_size > 0)
src_image = src_image.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
QtUtils::scaleMemoryCardIconWithSharpBilinear(src_image, icon_size);
src_image.setDevicePixelRatio(dpr);

@ -116,6 +116,22 @@ QIcon GetIconForEntryType(GameList::EntryType type);
QIcon GetIconForCompatibility(GameDatabase::CompatibilityRating rating);
QIcon GetIconForLanguage(std::string_view language_name);
/// Scales a Memory Card Icon (QPixmap or QImage) using Sharp Bilinear scaling
template<typename T>
inline void scaleMemoryCardIconWithSharpBilinear(T& pm, int size)
{
const int base_size = 16;
// Sharp Bilinear scaling
// First, scale the icon by the largest integer size using nearest-neighbor...
const int integer_icon_size = static_cast<int>(size / base_size) * base_size;
pm = pm.scaled(integer_icon_size, integer_icon_size, Qt::IgnoreAspectRatio, Qt::FastTransformation);
// ...then scale any remainder using bilinear interpolation.
if (size - integer_icon_size > 0)
pm = pm.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
/// Returns the pixel ratio/scaling factor for a widget.
qreal GetDevicePixelRatioForWidget(const QWidget* widget);

Loading…
Cancel
Save