feat: Font Size & Game Icon Scaling & misc. UI Fixes & loading animation for Grid View game parsing

This commit is contained in:
collecting
2026-04-20 02:37:09 -04:00
parent 7760d4a7ef
commit cef5432584
12 changed files with 426 additions and 117 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2400/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 5L5 17h2.2l1.4-3.8h6.8L16.8 17H19L14 5h-4zm-.4 6.7L12 6l2.4 5.7H9.6zM18.5 12c-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5 2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2400/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 5H3c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 12H3V7h18v10zm-6.5-6.5L11 14l-2.5-3.5L5 15h14l-4.5-5.5z"/>
<circle cx="8" cy="9.5" r="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

+2
View File
@@ -13,6 +13,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<file alias="dist/carousel.svg">../../dist/carousel.svg</file>
<file alias="dist/list.svg">../../dist/list.svg</file>
<file alias="dist/grid.svg">../../dist/grid.svg</file>
<file alias="dist/font_size.svg">../../dist/font_size.svg</file>
<file alias="dist/game_icon.svg">../../dist/game_icon.svg</file>
<file alias="dist/sort_ascending.svg">../../dist/sort_ascending.svg</file>
<file alias="dist/sort_descending.svg">../../dist/sort_descending.svg</file>
<file alias="dist/controller_navigation.svg">../../dist/controller_navigation.svg</file>
+9 -6
View File
@@ -111,7 +111,8 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
const bool is_selected = option.state & QStyle::State_Selected;
QRect rect = option.rect;
const int icon_size = UISettings::values.game_icon_size.GetValue();
const float scale = static_cast<float>(icon_size) / 128.0f;
const float raw_scale = static_cast<float>(icon_size) / 128.0f;
const float scale = std::max(0.1f, raw_scale);
qreal entry_val = 1.0;
const QPersistentModelIndex key(index);
@@ -218,7 +219,7 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
painter->setPen(fav_gold);
QFont sf = painter->font();
sf.setBold(true);
sf.setPointSizeF(11.0f * scale);
sf.setPointSizeF(std::max(1.0f, 11.0f * scale));
painter->setFont(sf);
painter->drawText(star_rect.adjusted(0, -1 * scale, 0, 0), Qt::AlignCenter,
QStringLiteral(""));
@@ -238,16 +239,18 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
if (pixmap.isNull())
pixmap = index.data(Qt::DecorationRole).value<QPixmap>();
if (!pixmap.isNull()) {
QRectF mid_area(label_rect.left(), label_rect.top(), label_rect.width(),
bottom_bar.top() - label_rect.top());
painter->drawPixmap(mid_area, pixmap, pixmap.rect());
const qreal mid_h = bottom_bar.top() - label_rect.top();
if (mid_h > 0) {
QRectF mid_area(label_rect.left(), label_rect.top(), label_rect.width(), mid_h);
painter->drawPixmap(mid_area, pixmap, pixmap.rect());
}
}
QString title = index.data(Qt::DisplayRole).toString().split(QLatin1Char('\n')).first();
painter->setPen(Qt::white);
QFont tf = option.font;
tf.setBold(true);
tf.setPointSizeF(8.5f * scale);
tf.setPointSizeF(std::max(1.0f, 8.5f * scale));
painter->setFont(tf);
QRectF text_rect = bottom_bar.adjusted(10 * scale, 0, -10 * scale, 0);
+259 -88
View File
@@ -1346,109 +1346,190 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
connect(btn_carousel_view, &QToolButton::clicked,
[this]() { SetViewMode(GameList::ViewMode::Carousel); });
// Title/Icon size slider - compact with rounded corners
// Helper to create the small mode-toggle buttons flanking the slider
auto makeModeBtn = [&](const QString& label, const QString& tip) -> QToolButton* {
auto* btn = new QToolButton(toolbar);
btn->setText(label);
btn->setToolTip(tip);
btn->setCheckable(true);
btn->setAutoRaise(true);
btn->setFixedSize(26, 26);
btn->setStyleSheet(QStringLiteral(
"QToolButton {"
" border: 1px solid #3e3e42;"
" border-radius: 4px;"
" background: #2b2b2f;"
" color: #aaaaaa;"
" font-size: 9px; font-weight: bold;"
"}"
"QToolButton:hover { background: #3e3e42; color: #ffffff; }"
"QToolButton:checked {"
" background: #1a3a5c;"
" border-color: #0078d4;"
" color: #7ec8ff;"
"}"));
return btn;
};
// Font-size mode toggle
btn_slider_font_mode = makeModeBtn(QStringLiteral(""), tr("Slider: Font Size"));
QIcon font_icon(QStringLiteral(":/dist/font_size.svg"));
btn_slider_font_mode->setIcon(font_icon);
btn_slider_font_mode->setIconSize(QSize(16, 16));
btn_slider_font_mode->setChecked(true); // default mode
// Icon-size mode toggle
btn_slider_icon_mode = makeModeBtn(QStringLiteral(""), tr("Slider: Game Icon Size"));
QIcon img_icon(QStringLiteral(":/dist/game_icon.svg"));
btn_slider_icon_mode->setIcon(img_icon);
btn_slider_icon_mode->setIconSize(QSize(16, 16));
// Title/Icon size slider — range and initial value depend on active mode
slider_title_size = new QSlider(Qt::Horizontal, toolbar);
slider_title_size->setMinimum(32);
slider_title_size->setMaximum(256);
slider_title_size->setValue(static_cast<int>(UISettings::values.game_icon_size.GetValue()));
slider_title_size->setToolTip(tr("Game Icon Size"));
slider_title_size->setMaximumWidth(120);
slider_title_size->setMinimumWidth(120);
slider_title_size->setStyleSheet(QStringLiteral("QSlider::groove:horizontal {"
" border: 1px solid palette(mid);"
" height: 4px;"
" background: palette(base);"
" border-radius: 2px;"
"}"
"QSlider::handle:horizontal {"
" background: palette(button);"
" border: 1px solid palette(mid);"
" width: 12px;"
" height: 12px;"
" margin: -4px 0;"
" border-radius: 6px;"
"}"
"QSlider::handle:horizontal:hover {"
" background: palette(light);"
"}"));
slider_title_size->setMinimum(8); // font points (font mode)
slider_title_size->setMaximum(24);
// Initialise to current font size
slider_title_size->setValue(tree_view->font().pointSize());
slider_title_size->setToolTip(tr("Font Size"));
slider_title_size->setMaximumWidth(110);
slider_title_size->setMinimumWidth(110);
slider_title_size->setStyleSheet(QStringLiteral(
"QSlider::groove:horizontal {"
" border: 1px solid palette(mid);"
" height: 4px;"
" background: palette(base);"
" border-radius: 2px;"
"}"
"QSlider::handle:horizontal {"
" background: palette(button);"
" border: 1px solid palette(mid);"
" width: 12px; height: 12px;"
" margin: -4px 0;"
" border-radius: 6px;"
"}"
"QSlider::handle:horizontal:hover { background: palette(light); }"));
// Switch to font-size mode
connect(btn_slider_font_mode, &QToolButton::clicked, [this]() {
slider_icon_mode = false;
btn_slider_font_mode->setChecked(true);
btn_slider_icon_mode->setChecked(false);
slider_title_size->blockSignals(true);
slider_title_size->setRange(8, 24);
slider_title_size->setValue(qBound(8, tree_view->font().pointSize(), 24));
slider_title_size->setToolTip(tr("Font Size"));
slider_title_size->blockSignals(false);
});
// Switch to icon-size mode
connect(btn_slider_icon_mode, &QToolButton::clicked, [this]() {
slider_icon_mode = true;
btn_slider_icon_mode->setChecked(true);
btn_slider_font_mode->setChecked(false);
slider_title_size->blockSignals(true);
slider_title_size->setRange(32, 256);
slider_title_size->setValue(
static_cast<int>(UISettings::values.game_icon_size.GetValue()));
slider_title_size->setToolTip(tr("Game Icon Size"));
slider_title_size->blockSignals(false);
});
connect(slider_title_size, &QSlider::valueChanged, [this](int value) {
// Update title font size in tree view
QFont font = tree_view->font();
font.setPointSize(qBound(8, value / 8, 24));
tree_view->setFont(font);
tree_view->doItemsLayout(); // Force redraw and size recalculation
if (!slider_icon_mode) {
// ── Font-size mode ──────────────────────────────────────────────
QFont font = tree_view->font();
font.setPointSize(qBound(8, value, 24));
tree_view->setFont(font);
tree_view->doItemsLayout();
} else {
// ── Icon-size mode ──────────────────────────────────────────────
UISettings::values.game_icon_size.SetValue(static_cast<u32>(value));
// Immediately force the tree view to recalculate row heights and repaint
// so the game cards ("pills") scale dynamically without clipping.
tree_view->doItemsLayout();
if (grid_view) {
grid_view->UpdateGridSize();
}
if (carousel_view) {
carousel_view->update();
}
#ifndef __linux__
// On non-Linux platforms, also update game icon size and repaint grid view
UISettings::values.game_icon_size.SetValue(static_cast<u32>(value));
if (grid_view->isVisible()) {
QAbstractItemModel* current_model = grid_view->model();
if (current_model && current_model != item_model) {
QStandardItemModel* flat_model = qobject_cast<QStandardItemModel*>(current_model);
if (flat_model) {
const u32 icon_size = static_cast<u32>(value);
grid_view->view()->setGridSize(QSize(icon_size + 60, icon_size + 80));
int scroll_position = grid_view->view()->verticalScrollBar()->value();
QModelIndex current_index = grid_view->currentIndex();
if (grid_view->isVisible()) {
QAbstractItemModel* current_model = grid_view->model();
if (current_model && current_model != item_model) {
QStandardItemModel* flat_model =
qobject_cast<QStandardItemModel*>(current_model);
if (flat_model) {
const u32 icon_size = static_cast<u32>(value);
int scroll_position =
grid_view->view()->verticalScrollBar()->value();
QModelIndex current_index = grid_view->currentIndex();
for (int i = 0; i < flat_model->rowCount(); ++i) {
QStandardItem* item = flat_model->item(i);
if (item) {
u64 program_id =
item->data(GameListItemPath::ProgramIdRole).toULongLong();
QStandardItem* original_item = nullptr;
for (int folder_idx = 0; folder_idx < item_model->rowCount();
++folder_idx) {
QStandardItem* folder = item_model->item(folder_idx, 0);
if (!folder)
continue;
for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
QStandardItem* game = folder->child(game_idx, 0);
if (game &&
game->data(GameListItemPath::ProgramIdRole).toULongLong() ==
program_id) {
original_item = game;
break;
for (int i = 0; i < flat_model->rowCount(); ++i) {
QStandardItem* item = flat_model->item(i);
if (item) {
u64 program_id =
item->data(GameListItemPath::ProgramIdRole).toULongLong();
QStandardItem* original_item = nullptr;
for (int folder_idx = 0; folder_idx < item_model->rowCount();
++folder_idx) {
QStandardItem* folder = item_model->item(folder_idx, 0);
if (!folder)
continue;
for (int game_idx = 0; game_idx < folder->rowCount();
++game_idx) {
QStandardItem* game = folder->child(game_idx, 0);
if (game && game->data(GameListItemPath::ProgramIdRole)
.toULongLong() == program_id) {
original_item = game;
break;
}
}
if (original_item)
break;
}
if (original_item)
break;
}
if (original_item) {
QVariant orig_icon_data = original_item->data(Qt::DecorationRole);
if (orig_icon_data.isValid() &&
orig_icon_data.type() == QVariant::Pixmap) {
QPixmap orig_pixmap = orig_icon_data.value<QPixmap>();
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
if (original_item) {
QVariant orig_icon_data =
original_item->data(Qt::DecorationRole);
if (orig_icon_data.isValid() &&
orig_icon_data.type() == QVariant::Pixmap) {
QPixmap orig_pixmap =
orig_icon_data.value<QPixmap>();
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius,
radius);
painter.setClipPath(path);
QPixmap scaled = orig_pixmap.scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
}
}
}
}
if (scroll_position >= 0)
grid_view->view()->verticalScrollBar()->setValue(scroll_position);
if (current_index.isValid() &&
current_index.row() < flat_model->rowCount())
grid_view->setCurrentIndex(
flat_model->index(current_index.row(), 0));
}
if (scroll_position >= 0) {
grid_view->view()->verticalScrollBar()->setValue(scroll_position);
}
if (current_index.isValid() && current_index.row() < flat_model->rowCount()) {
grid_view->setCurrentIndex(flat_model->index(current_index.row(), 0));
}
} else {
PopulateGridView();
}
} else {
PopulateGridView();
}
}
#endif
}
});
// A-Z sort button - positioned after slider
@@ -1511,6 +1592,8 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
toolbar_layout->addWidget(btn_list_view);
toolbar_layout->addWidget(btn_grid_view);
toolbar_layout->addWidget(btn_carousel_view);
toolbar_layout->addWidget(btn_slider_font_mode);
toolbar_layout->addWidget(btn_slider_icon_mode);
toolbar_layout->addWidget(slider_title_size);
toolbar_layout->addWidget(btn_sort_az);
toolbar_layout->addWidget(btn_surprise_me);
@@ -1932,6 +2015,82 @@ void GameList::AddEntry(const QList<QStandardItem*>& entry_items, const QString&
if (delegate) {
delegate->RegisterEntryAnimation(entry_items.first()->index());
}
// Dynamically populate grid and carousel views so games appear instantly when parsing
QStandardItem* src = entry_items.first();
QString path = src->data(GameListItemPath::FullPathRole).toString();
if (!UISettings::values.hidden_paths.contains(path)) {
QString title = src->data(GameListItemPath::TitleRole).toString();
if (title.isEmpty()) {
std::string fn;
Common::SplitPath(path.toStdString(), nullptr, &fn, nullptr);
title = QString::fromStdString(fn);
}
QString filter_text = search_field->filterText().toLower();
if (filter_text.isEmpty() || ContainsAllWords(title.toLower(), filter_text)) {
u64 pid = src->data(GameListItemPath::ProgramIdRole).toULongLong();
bool is_fav = UISettings::values.favorited_ids.contains(pid);
auto cloneToFlatModel = [&](QStandardItemModel* target, bool force_fav) {
if (!target) return;
QStandardItem* item = src->clone();
item->setText(title);
item->setData(title, GameListItemPath::SortRole);
item->setData(static_cast<int>(force_fav ? GameListItemType::Favorites : GameListItemType::Game), GameListItem::TypeRole);
// Scale icon for flat layout
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
QVariant icon_data = item->data(Qt::DecorationRole);
if (icon_data.isValid() && icon_data.canConvert<QPixmap>()) {
QPixmap pixmap = icon_data.value<QPixmap>();
if (!pixmap.isNull()) {
item->setData(pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
}
}
target->appendRow(item);
// We typically want to sort grid models, but NOT the carousel (it uses its own segments)
if (target != static_cast<QStandardItemModel*>(carousel_view->view()->model())) {
target->sort(0, current_sort_order);
}
// Trigger "Pop-in" Animations for discovery
// Use !isHidden() instead of isVisible() to ensure it triggers during boot-time startup
if (grid_view && !grid_view->isHidden()) {
QListView* active_grid = force_fav ? grid_view->favView() : grid_view->view();
if (active_grid) {
auto* grid_delegate = qobject_cast<GameGridDelegate*>(active_grid->itemDelegate());
if (grid_delegate) {
// Find the new index in the target model after sorting to ensure animation triggers on the correct item
QModelIndex new_idx;
auto matches = target->match(target->index(0, 0), GameListItemPath::FullPathRole, path, 1, Qt::MatchExactly);
if (!matches.isEmpty()) {
new_idx = matches.first();
grid_delegate->RegisterEntryAnimation(new_idx);
}
}
}
}
if (carousel_view && !carousel_view->isHidden() && target == static_cast<QStandardItemModel*>(carousel_view->view()->model())) {
carousel_view->view()->RegisterEntryAnimation(target->index(target->rowCount() - 1, 0));
}
};
if (grid_view && grid_view->mainModel()) {
QStandardItemModel* target_model = static_cast<QStandardItemModel*>(is_fav ? grid_view->favModel() : grid_view->mainModel());
cloneToFlatModel(target_model, is_fav);
grid_view->UpdateGridSize();
}
if (carousel_view && carousel_view->view()->model()) {
QStandardItemModel* target_model = static_cast<QStandardItemModel*>(carousel_view->view()->model());
cloneToFlatModel(target_model, is_fav);
}
}
}
}
// Auto-scroll to show new games as they appear (only on first-time or full rebuild)
@@ -3274,6 +3433,10 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs, bool is_sm
item_model->clear();
item_model->insertColumns(0, COLUMN_COUNT);
RetranslateUI();
// Ensure flat models for Grid/Carousel are instantiated empty BEFORE async parsing starts
// so that games can be visually streamed straight into them during boot.
FilterGridView(search_field->filterText());
} else {
// Targeted metadata updates are now handled by RefreshGame() or worker signals
// to avoid global UI-wide 'refreshing' indicators that can feel unresponsive.
@@ -3859,6 +4022,14 @@ void GameList::UpdateAccentColorStyles() {
if (btn_carousel_view)
btn_carousel_view->setStyleSheet(icon_button_style + button_checked_style);
// Apply accent style to our new slider toggles so they match the other tool buttons
if (btn_slider_font_mode) {
btn_slider_font_mode->setStyleSheet(icon_button_style + button_checked_style);
}
if (btn_slider_icon_mode) {
btn_slider_icon_mode->setStyleSheet(icon_button_style + button_checked_style);
}
// Also update Sort, Surprise Me and Controller buttons with the locked dark style
btn_sort_az->setStyleSheet(icon_button_style);
btn_surprise_me->setStyleSheet(icon_button_style);
+3
View File
@@ -254,6 +254,9 @@ private:
QToolButton* btn_grid_view = nullptr;
QToolButton* btn_carousel_view = nullptr;
QSlider* slider_title_size = nullptr;
QToolButton* btn_slider_font_mode = nullptr;
QToolButton* btn_slider_icon_mode = nullptr;
bool slider_icon_mode = false; // false = font size, true = icon size
QToolButton* btn_sort_az = nullptr;
QToolButton* btn_surprise_me = nullptr;
QToolButton* btn_controller_settings = nullptr;
+29 -2
View File
@@ -26,6 +26,7 @@
#include <QVBoxLayout>
#include <QWheelEvent>
#include <QWidget>
#include <QWindow>
#include <QEvent>
@@ -114,6 +115,16 @@ void OnyxTooltip::showText(const QPoint& pos, const QString& text, QWidget* w) {
showPos.setY(pos.y() - s_onyx_tooltip_instance->height() - 10);
}
// Wayland fix: Popups must have a transient parent to be positioned correctly
if (w && w->window()) {
s_onyx_tooltip_instance->winId(); // Ensure window handle is created
if (auto* window = s_onyx_tooltip_instance->windowHandle()) {
if (auto* parent_window = w->window()->windowHandle()) {
window->setTransientParent(parent_window);
}
}
}
s_onyx_tooltip_instance->move(showPos);
s_onyx_tooltip_instance->show();
}
@@ -159,7 +170,21 @@ QSize GameListDelegate::sizeHint(const QStyleOptionViewItem& option,
const bool is_game_row = index.parent().isValid();
if (is_game_row) {
size.setHeight(GetCardHeight());
int base_height = GetCardHeight();
// Calculate the height needed for the text based on current font metrics
QFontMetrics title_metrics(option.font);
const int title_h = title_metrics.ascent() + title_metrics.descent();
QFont f_id = option.font;
f_id.setPointSize(std::max(6, f_id.pointSize() - 2));
QFontMetrics id_metrics(f_id);
const int id_h = id_metrics.ascent() + id_metrics.descent();
// 6px spacing between lines + 24px total top/bottom margin
const int text_required_height = title_h + id_h + 6 + 24;
size.setHeight(std::max(base_height, text_required_height));
} else {
size.setHeight(36); // Clean height for folder headers
}
@@ -552,7 +577,9 @@ void GameListDelegate::PaintGameInfo(QPainter* painter, const QRect& rect,
icon_path.addRoundedRect(icon_rect, 6, 6);
painter->save();
painter->setClipPath(icon_path);
painter->drawPixmap(icon_rect, pixmap.scaled(icon_size, icon_size, Qt::KeepAspectRatio,
const int final_icon_size = std::max(1, icon_size);
painter->drawPixmap(icon_rect, pixmap.scaled(final_icon_size, final_icon_size, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
painter->restore();
+66 -13
View File
@@ -22,7 +22,26 @@ CinematicCarousel::CinematicCarousel(QWidget* parent) : QWidget(parent) {
m_snap_animation->setEasingCurve(QEasingCurve::OutCubic);
m_pulse_timer = new QTimer(this);
connect(m_pulse_timer, &QTimer::timeout, this, [this]{ m_pulse_tick++; update(); });
connect(m_pulse_timer, &QTimer::timeout, this, [this]{
m_pulse_tick++;
// Advance entry animations
auto it = m_entry_animations.begin();
while (it != m_entry_animations.end()) {
if (!it.key().isValid()) {
it = m_entry_animations.erase(it);
continue;
}
if (it.value() < 1.0) {
it.value() += 0.06;
if (it.value() >= 1.0) it.value() = 1.0;
++it;
} else {
it = m_entry_animations.erase(it);
}
}
update();
});
m_pulse_timer->start(32);
m_scroll_timer = new QTimer(this);
@@ -85,8 +104,19 @@ QRect CinematicCarousel::visualRect(const QModelIndex& index) const {
return QRect(x - (cs * s) / 2.0, vcy - (cs * s) / 2.0, cs * s, cs * s);
}
void CinematicCarousel::setModel(QAbstractItemModel* model) { m_model = model; if (m_model && m_model->rowCount() > 0) setFocalIndex(0.0); update(); }
void CinematicCarousel::setModel(QAbstractItemModel* model) {
if (m_model) {
m_model->disconnect(this);
}
m_model = model;
if (m_model) {
connect(m_model, &QAbstractItemModel::rowsInserted, this, [this]() { update(); });
connect(m_model, &QAbstractItemModel::modelReset, this, [this]() { update(); });
connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() { update(); });
}
if (m_model && m_model->rowCount() > 0) setFocalIndex(0.0);
update();
}
void CinematicCarousel::setFocalIndex(qreal index) {
if (!m_model || m_model->rowCount() == 0) m_focal_index = 0.0;
else m_focal_index = std::max(0.0, std::min(static_cast<qreal>(m_model->rowCount() - 1), index));
@@ -95,6 +125,12 @@ void CinematicCarousel::setFocalIndex(qreal index) {
void CinematicCarousel::scrollTo(int index) { if (!m_model || index < 0 || index >= m_model->rowCount()) return; startSnapAnimation(index); }
void CinematicCarousel::RegisterEntryAnimation(const QModelIndex& index) {
if (index.isValid()) {
m_entry_animations[QPersistentModelIndex(index)] = 0.0;
}
}
void CinematicCarousel::scrollToLetter(QChar letter) {
if (!m_model) return;
for (int i = 0; i < m_model->rowCount(); ++i) {
@@ -118,10 +154,10 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
QPainter p(this); p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const int count = m_model->rowCount(); const qreal vcx = width() / 2.0; const qreal vcy = height() / 2.0;
const int is = UISettings::values.game_icon_size.GetValue();
const float scale = static_cast<float>(is) / 128.0f;
const float raw_scale = static_cast<float>(is) / 128.0f;
const float scale = std::max(0.1f, raw_scale);
const qreal bs = is + (35.0f * scale);
const qreal th = is * 2.0;
const qreal arrow_ay = std::max(55.0, vcy - (1.4 * ((is + static_cast<int>(25 * scale)) / 2.0)) - (28.0f * scale));
const qreal arrow_ay = std::max(35.0, vcy - (1.4 * ((is + static_cast<int>(25 * scale)) / 2.0)) - (28.0f * scale));
QVector<int> order;
for (int i = 0; i < count; ++i) order << i;
@@ -130,9 +166,22 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
const qreal d = i - m_focal_index; const qreal dist = std::abs(d * bs);
if (dist > width()) continue;
qreal s = 1.0; qreal dx = 0.0;
if (dist < th) { qreal f = 1.0 - (dist / th); s = 1.0 + (f * 0.40); dx = d * (is / 2.0) * f; }
const qreal x = vcx + (d * bs) + dx; const qreal y = vcy;
p.save(); p.translate(x, y); p.scale(s, s);
p.save(); p.translate(x, y);
// Apply Entry Animation (discovery "pop" effect)
qreal entry_anim = 1.0;
QPersistentModelIndex pidx(m_model->index(i, 0));
if (m_entry_animations.contains(pidx)) {
entry_anim = m_entry_animations[pidx];
}
if (entry_anim < 1.0) {
qreal pop_s = 0.7 + (entry_anim * 0.3);
p.scale(pop_s, pop_s);
p.setOpacity(entry_anim);
}
p.scale(s, s);
const int cs_w = is + static_cast<int>(25 * scale);
const int cs_h = is + static_cast<int>(25 * scale);
QRectF cr(-cs_w / 2.0, -cs_h / 2.0, cs_w, cs_h);
@@ -166,14 +215,16 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
qreal header_ay = arrow_ay - (15.0f * scale);
if (is_fav) {
QFont hf = font(); hf.setBold(true); hf.setPointSizeF(24.0f * scale); p.setFont(hf);
p.drawText(QRectF(x - 150 * scale, header_ay - 90 * scale, 300 * scale, 60 * scale), Qt::AlignCenter, tr("★ FAVORITES"));
QFont hf = font(); hf.setBold(true); hf.setPointSizeF(std::max(1.0f, 24.0f * scale)); p.setFont(hf);
QRectF text_rect(x - 300 * scale, header_ay - 150 * scale, 600 * scale, 150 * scale);
p.drawText(text_rect, Qt::AlignHCenter | Qt::AlignBottom | Qt::TextDontClip, tr("★ FAVORITES"));
} else {
QString title = m_model->index(i, 0).data(Qt::DisplayRole).toString();
QChar cl = title.isEmpty() ? QLatin1Char('#') : title[0].toUpper();
if (!cl.isLetter()) cl = QLatin1Char('#');
QFont hf = font(); hf.setBold(true); hf.setPointSizeF(48.0f * scale); p.setFont(hf);
p.drawText(QRectF(x - 80 * scale, header_ay - 110 * scale, 160 * scale, 100 * scale), Qt::AlignCenter, cl);
QFont hf = font(); hf.setBold(true); hf.setPointSizeF(std::max(1.0f, 48.0f * scale)); p.setFont(hf);
QRectF text_rect(x - 200 * scale, header_ay - 200 * scale, 400 * scale, 200 * scale);
p.drawText(text_rect, Qt::AlignHCenter | Qt::AlignBottom | Qt::TextDontClip, cl);
}
p.restore();
}
@@ -351,6 +402,7 @@ GameCarouselView::GameCarouselView(QWidget* parent) : QWidget(parent) {
m_top_hint = new QLabel(this);
m_top_hint->setText(tr("if using controller* Press X for Next Alphabetical Letter | Press -/R/ZR for Details Tab | Press B for Back to List"));
m_top_hint->setAlignment(Qt::AlignCenter);
m_top_hint->setWordWrap(true);
m_layout->addSpacing(10);
m_layout->addWidget(m_top_hint);
m_layout->addSpacing(30);
@@ -361,6 +413,7 @@ GameCarouselView::GameCarouselView(QWidget* parent) : QWidget(parent) {
m_bottom_hint = new QLabel(this);
m_bottom_hint->setText(tr("*You can Drag to Scroll, or Click on Game Icons manually, you can also use your mouse wheel!*"));
m_bottom_hint->setAlignment(Qt::AlignCenter);
m_bottom_hint->setWordWrap(true);
m_layout->addWidget(m_bottom_hint);
connect(m_carousel, &CinematicCarousel::focalItemChanged, this, &GameCarouselView::itemSelectionChanged);
connect(m_carousel, &CinematicCarousel::itemActivated, this, &GameCarouselView::itemActivated);
@@ -382,4 +435,4 @@ void GameCarouselView::ApplyTheme() {
}
}
void GameCarouselView::setModel(QAbstractItemModel* model) { m_carousel->setModel(model); }
void GameCarouselView::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); m_carousel->setMinimumHeight(UISettings::values.game_icon_size.GetValue() + 380); }
void GameCarouselView::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); m_carousel->setMinimumHeight((UISettings::values.game_icon_size.GetValue() * 2.0) + 450); }
+3
View File
@@ -29,6 +29,7 @@ public:
void scrollTo(int index);
void scrollToLetter(QChar letter);
void RegisterEntryAnimation(const QModelIndex& index);
void ApplyTheme();
void setControllerFocus(bool focus);
@@ -86,6 +87,8 @@ private:
QTimer* m_momentum_timer = nullptr;
qreal m_velocity = 0.0;
qint64 m_last_move_timestamp = 0;
mutable QMap<QPersistentModelIndex, qreal> m_entry_animations;
};
class GameCarouselView : public QWidget {
+40 -7
View File
@@ -175,6 +175,7 @@ GameGridView::GameGridView(QWidget* parent) : QWidget(parent) {
m_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scroll_area->setStyleSheet(QStringLiteral("QScrollArea { background: transparent; }"));
m_scroll_area->viewport()->installEventFilter(this);
m_container = new WoodBackgroundContainer(m_scroll_area);
m_layout = new QVBoxLayout(m_container);
@@ -455,6 +456,14 @@ void GameGridView::keyPressEvent(QKeyEvent* event) {
}
}
bool GameGridView::eventFilter(QObject* obj, QEvent* event) {
if (m_scroll_area && obj == m_scroll_area->viewport() && event->type() == QEvent::Resize) {
// Trigger grid size update slightly later to let layout settle, preventing feedback loop
QTimer::singleShot(10, this, &GameGridView::UpdateGridSize);
}
return QWidget::eventFilter(obj, event);
}
void GameGridView::showEvent(QShowEvent* event) {
QWidget::showEvent(event);
QTimer::singleShot(10, this, &GameGridView::UpdateGridSize);
@@ -479,22 +488,46 @@ void GameGridView::UpdateGridSize() {
return;
}
if (tw > 50) {
int cols = qMax(1, tw / bw);
int aw = tw / cols;
const int item_h = qMax(is + static_cast<int>(85 * s), is + 40);
QSize gs(aw, item_h);
int fav_count = m_fav_view->model() ? m_fav_view->model()->rowCount() : 0;
int main_count = m_main_view->model() ? m_main_view->model()->rowCount() : 0;
if (tw == m_last_tw && is == m_last_is && fav_count == m_last_fav_count && main_count == m_last_main_count) {
return;
}
m_last_tw = tw;
m_last_is = is;
m_last_fav_count = fav_count;
m_last_main_count = main_count;
// Ensure the entire Grid View widget never compresses horizontally below 1 column.
// + 30 for scrollbar allowance, minimizing grid squishing side-effects.
setMinimumWidth(bw + 30);
int cols = qMax(1, tw / bw);
int aw = tw / cols;
const int item_h = qMax(is + static_cast<int>(85 * s), is + 40);
QSize gs(aw, item_h);
// Unhide Favorites section if we now have items during discovery
if (fav_count > 0 && m_fav_view->isHidden()) {
m_fav_label->show();
m_fav_view->show();
}
m_fav_view->setGridSize(gs);
m_main_view->setGridSize(gs);
m_fav_view->setFixedWidth(tw);
m_main_view->setFixedWidth(tw);
}
m_fav_view->doItemsLayout();
m_main_view->doItemsLayout();
m_fav_view->viewport()->update();
m_main_view->viewport()->update();
UpdateLayoutHeights();
}
void GameGridView::UpdateLayoutHeights() {
auto updateHeight = [&](QListView* view) {
if (!view || !view->isVisible() || !view->model() || view->model()->rowCount() == 0) {
if (!view || !view->model() || view->model()->rowCount() == 0) {
view->setFixedHeight(0);
return;
}
+7 -1
View File
@@ -43,6 +43,8 @@ public:
void setControllerFocus(bool focus);
bool hasControllerFocus() const { return m_has_focus; }
void UpdateGridSize();
public slots:
void onNavigated(int dx, int dy);
void onActivated();
@@ -56,10 +58,14 @@ protected:
void keyPressEvent(QKeyEvent* event) override;
void showEvent(QShowEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
bool eventFilter(QObject* obj, QEvent* event) override;
private:
void UpdateGridSize();
void UpdateLayoutHeights();
int m_last_tw = -1;
int m_last_is = -1;
int m_last_fav_count = -1;
int m_last_main_count = -1;
QScrollArea* m_scroll_area = nullptr;
QWidget* m_container = nullptr;
+1
View File
@@ -251,6 +251,7 @@ void UpdaterDialog::SetupUI() {
// Kill ghost titles causing overlaps
ui->updateInfoGroup->setTitle(QString());
ui->changelogGroup->setTitle(QString());
ui->progressGroup->setTitle(QString());
// Global Alignment & Centering
ui->titleLabel->setAlignment(Qt::AlignCenter);