mirror of
https://github.com/citron-neo/emulator.git
synced 2026-07-05 15:21:57 -07:00
feat: Font Size & Game Icon Scaling & misc. UI Fixes & loading animation for Grid View game parsing
This commit is contained in:
Vendored
+3
@@ -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 |
Vendored
+4
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user