feat: Carousel & Controller Nav. SVG's / Added Favorites Column to Grid/Carousel / Various Bug Fixes

This commit is contained in:
collecting
2026-04-05 19:03:12 -04:00
parent 5068853915
commit 256c924de8
10 changed files with 538 additions and 309 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<rect x="0.5" y="6" width="7" height="13" rx="2" fill="#848484"/>
<rect x="16.5" y="6" width="7" height="13" rx="2" fill="#848484"/>
<rect x="8" y="3" width="8" height="18" rx="2" fill="#FEFEFE"/>
</svg>

After

Width:  |  Height:  |  Size: 300 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<rect x="1" y="7" width="22" height="12" rx="5" fill="#FEFEFE"/>
<rect x="2.5" y="8.5" width="19" height="9" rx="3.5" fill="#1E1E1E"/>
<rect x="4.5" y="11.25" width="5" height="1.5" rx="0.75" fill="#FEFEFE"/>
<rect x="6.5" y="9.25" width="1.5" height="5.5" rx="0.75" fill="#FEFEFE"/>
<circle cx="15.5" cy="10.75" r="1.1" fill="#FEFEFE"/>
<circle cx="17.5" cy="12.5" r="1.1" fill="#FEFEFE"/>
<circle cx="15.5" cy="14.25" r="1.1" fill="#FEFEFE"/>
<circle cx="13.5" cy="12.5" r="1.1" fill="#FEFEFE"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

+2
View File
@@ -10,6 +10,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</qresource>
<qresource prefix="/">
<file alias="dist/dice.svg">../../dist/dice.svg</file>
<file alias="dist/carousel.svg">../../dist/carousel.svg</file>
<file alias="dist/controller_navigation.svg">../../dist/controller_navigation.svg</file>
<file alias="citron.svg">../../dist/citron.svg</file>
</qresource>
</RCC>
+44 -17
View File
@@ -31,7 +31,8 @@ void GameGridDelegate::setGridMode(GridMode mode) {
QSize GameGridDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const {
const int icon_size = UISettings::values.game_icon_size.GetValue();
return QSize(icon_size + 40, icon_size + 85);
const float scale = static_cast<float>(icon_size) / 128.0f;
return QSize(icon_size + static_cast<int>(40 * scale), icon_size + static_cast<int>(85 * scale));
}
void GameGridDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
@@ -98,6 +99,8 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
const QModelIndex& index) const {
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;
qreal entry_val = 1.0;
const QPersistentModelIndex key(index);
@@ -106,10 +109,12 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
qreal final_opacity = entry_val * m_population_fade_global;
painter->setOpacity(final_opacity);
QRect card_rect = rect.adjusted(12, 12, -12, -12);
const int icon_size = UISettings::values.game_icon_size.GetValue();
const int card_h = icon_size + 64;
card_rect.setHeight(card_h);
const int card_w = icon_size + static_cast<int>(16 * scale);
const int card_h = icon_size + static_cast<int>(64 * scale);
const int cx = rect.x() + (rect.width() - card_w) / 2;
QRect card_rect(cx, rect.y() + static_cast<int>(12 * scale), card_w, card_h);
const int radius = static_cast<int>(14 * scale);
if (is_selected) {
QColor glow = AccentColor();
@@ -117,31 +122,53 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
painter->save();
painter->setBrush(glow);
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(card_rect.adjusted(-4, -4, 4, 4), 16, 16);
painter->drawRoundedRect(card_rect.adjusted(static_cast<int>(-4 * scale), static_cast<int>(-4 * scale),
static_cast<int>(4 * scale), static_cast<int>(4 * scale)),
radius + 2, radius + 2);
painter->restore();
}
QColor onyx(22, 22, 26);
painter->setPen(Qt::NoPen);
painter->setBrush(onyx);
painter->drawRoundedRect(card_rect, 14, 14);
painter->drawRoundedRect(card_rect, radius, radius);
if (is_selected) {
QColor border = AccentColor();
qreal pulse = (m_pulse_states.contains(key)) ? m_pulse_states[key] : 0.0;
painter->setPen(QPen(border, 3.5 + pulse * 1.5));
painter->setPen(QPen(border, (3.5f + pulse * 1.5f) * scale));
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(card_rect, 14, 14);
painter->drawRoundedRect(card_rect, radius, radius);
}
QRectF label_rect = card_rect.adjusted(6, 6, -6, -22);
QRectF label_rect = card_rect.adjusted(6 * scale, 6 * scale, -6 * scale, -22 * scale);
QPainterPath label_path;
label_path.addRoundedRect(label_rect, 8, 8);
label_path.addRoundedRect(label_rect, radius - 6, radius - 6);
// --- Favorites Indicator ---
bool is_fav = (index.data(GameListItem::TypeRole).toInt() == static_cast<int>(GameListItemType::Favorites));
if (is_fav) {
painter->save();
QColor fav_gold(255, 215, 0, 220); // Vibrant Gold
painter->setPen(QPen(fav_gold, 1.2f * scale));
painter->setBrush(QColor(40, 40, 45, 180));
qreal star_size = 22.0f * scale;
QRectF star_rect(card_rect.right() - (10.0f * scale) - star_size, card_rect.top() + (10.0f * scale),
star_size, star_size);
painter->drawRoundedRect(star_rect, 6 * scale, 6 * scale);
painter->setPen(fav_gold);
QFont sf = painter->font(); sf.setBold(true); sf.setPointSizeF(11.0f * scale); painter->setFont(sf);
painter->drawText(star_rect.adjusted(0, -1 * scale, 0, 0), Qt::AlignCenter, QStringLiteral(""));
painter->restore();
}
// Removed old vertical divider logic in favor of section headers
painter->save();
painter->setClipPath(label_path);
QRectF bottom_bar(label_rect.left(), label_rect.bottom() - 28, label_rect.width(), 28);
qreal bar_h = 28.0f * scale;
QRectF bottom_bar(label_rect.left(), label_rect.bottom() - bar_h, label_rect.width(), bar_h);
painter->fillRect(bottom_bar, QColor(10, 10, 12));
QPixmap pixmap = index.data(GameListItemPath::HighResIconRole).value<QPixmap>();
@@ -155,21 +182,21 @@ void GameGridDelegate::PaintGridItem(QPainter* painter, const QStyleOptionViewIt
painter->setPen(Qt::white);
QFont tf = option.font;
tf.setBold(true);
tf.setPointSizeF(8.5);
tf.setPointSizeF(8.5f * scale);
painter->setFont(tf);
QRectF text_rect = bottom_bar.adjusted(10, 0, -10, 0);
QRectF text_rect = bottom_bar.adjusted(10 * scale, 0, -10 * scale, 0);
QString elided = painter->fontMetrics().elidedText(title, Qt::ElideRight, text_rect.width());
painter->drawText(text_rect, Qt::AlignCenter, elided);
painter->restore();
int pins_y = card_rect.bottom() - 10;
int pins_y = card_rect.bottom() - static_cast<int>(10 * scale);
int p_c = 6;
int p_w = (card_rect.width() - 40) / p_c;
int p_w = (card_rect.width() - static_cast<int>(40 * scale)) / p_c;
painter->setOpacity(0.8 * final_opacity);
for (int i = 0; i < p_c; ++i) {
painter->fillRect(card_rect.left() + 20 + i * p_w, pins_y, p_w - 6, 6, QColor(40, 40, 45));
painter->fillRect(card_rect.left() + static_cast<int>(20 * scale) + i * p_w, pins_y, p_w - static_cast<int>(6 * scale), static_cast<int>(6 * scale), QColor(40, 40, 45));
}
}
+154 -127
View File
@@ -721,106 +721,107 @@ void GameList::OnTextChanged(const QString& new_text) {
}
void GameList::FilterGridView(const QString& filter_text) {
auto cleanup = [&](QAbstractItemModel* m) { if (m && m != item_model) m->deleteLater(); };
cleanup(grid_view->favModel());
cleanup(grid_view->mainModel());
cleanup(carousel_view->model());
QStandardItemModel* hierarchical_model = item_model;
QStandardItemModel* flat_model = nullptr;
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
QAbstractItemModel* current_model = grid_view->view()->model();
if (current_model && current_model != item_model) {
QStandardItemModel* existing_flat = qobject_cast<QStandardItemModel*>(current_model);
if (existing_flat) {
existing_flat->clear();
flat_model = existing_flat;
}
}
int visible_count = 0, total_count = 0;
QStandardItemModel* fav_model = new QStandardItemModel(this);
QStandardItemModel* main_model = new QStandardItemModel(this);
QStandardItemModel* carousel_model = new QStandardItemModel(this);
QSet<QString> fav_paths;
QStandardItem* h_fav_folder = nullptr;
if (!flat_model) {
if (current_model && current_model != item_model) {
current_model->deleteLater();
}
flat_model = new QStandardItemModel(this);
}
int visible_count = 0;
int total_count = 0;
for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
QStandardItem* folder = hierarchical_model->item(i, 0);
if (!folder || folder->data(GameListItem::TypeRole).value<GameListItemType>() ==
GameListItemType::AddDir) {
continue;
}
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game_item = folder->child(j, 0);
if (!game_item || game_item->data(GameListItem::TypeRole).value<GameListItemType>() !=
GameListItemType::Game)
continue;
total_count++;
const QString full_path = game_item->data(GameListItemPath::FullPathRole).toString();
bool should_show = !UISettings::values.hidden_paths.contains(full_path);
if (should_show && !filter_text.isEmpty()) {
const QString file_title =
game_item->data(GameListItemPath::TitleRole).toString().toLower();
const auto program_id =
game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
const QString file_program_id =
QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
const QString file_name =
full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() +
QLatin1Char{' '} + file_title;
should_show =
ContainsAllWords(file_name, filter_text) ||
(file_program_id.size() == 16 && file_program_id.contains(filter_text));
}
if (should_show) {
QStandardItem* cloned_item = game_item->clone();
QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
if (game_title.isEmpty()) {
std::string filename;
Common::SplitPath(full_path.toStdString(), nullptr, &filename, nullptr);
game_title = QString::fromStdString(filename);
}
cloned_item->setText(game_title);
flat_model->appendRow(cloned_item);
visible_count++;
}
if (hierarchical_model->item(i)->data(GameListItem::TypeRole).value<GameListItemType>() == GameListItemType::Favorites) {
h_fav_folder = hierarchical_model->item(i); break;
}
}
grid_view->setModel(flat_model);
carousel_view->setModel(flat_model);
flat_model->setSortRole(GameListItemPath::SortRole);
flat_model->sort(0, current_sort_order);
for (int i = 0; i < flat_model->rowCount(); ++i) {
QStandardItem* item = flat_model->item(i);
if (item) {
auto cloneTo = [&](QStandardItem* src, QStandardItemModel* target, bool force_fav = false) {
QStandardItem* item = src->clone();
QString title = src->data(GameListItemPath::TitleRole).toString();
if (title.isEmpty()) {
std::string fn; Common::SplitPath(src->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &fn, nullptr);
title = QString::fromStdString(fn);
}
item->setText(title);
item->setData(title, GameListItemPath::SortRole);
if (force_fav) {
item->setData(static_cast<int>(GameListItemType::Favorites), GameListItem::TypeRole);
} else {
item->setData(static_cast<int>(GameListItemType::Game), GameListItem::TypeRole);
}
target->appendRow(item);
};
auto isFavorite = [&](QStandardItem* item) {
u64 pid = item->data(GameListItemPath::ProgramIdRole).toULongLong();
return UISettings::values.favorited_ids.contains(pid);
};
if (h_fav_folder) {
for (int j = 0; j < h_fav_folder->rowCount(); ++j) {
QStandardItem* game = h_fav_folder->child(j, 0);
if (!game || game->data(GameListItem::TypeRole).value<GameListItemType>() != GameListItemType::Game) continue;
if (!isFavorite(game)) continue;
total_count++;
QString path = game->data(GameListItemPath::FullPathRole).toString();
if (UISettings::values.hidden_paths.contains(path)) continue;
if (!filter_text.isEmpty()) {
QString title = game->data(GameListItemPath::TitleRole).toString().toLower();
if (!ContainsAllWords(title, filter_text.toLower())) continue;
}
cloneTo(game, fav_model, true);
cloneTo(game, carousel_model, true);
fav_paths.insert(path);
visible_count++;
}
}
for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
QStandardItem* folder = hierarchical_model->item(i);
if (!folder || folder->data(GameListItem::TypeRole).value<GameListItemType>() == GameListItemType::AddDir ||
folder->data(GameListItem::TypeRole).value<GameListItemType>() == GameListItemType::Favorites) continue;
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game = folder->child(j, 0);
if (!game || game->data(GameListItem::TypeRole).value<GameListItemType>() != GameListItemType::Game) continue;
total_count++;
QString path = game->data(GameListItemPath::FullPathRole).toString();
if (fav_paths.contains(path) || UISettings::values.hidden_paths.contains(path)) continue;
if (!filter_text.isEmpty()) {
QString title = game->data(GameListItemPath::TitleRole).toString().toLower();
if (!ContainsAllWords(title, filter_text.toLower())) continue;
}
cloneTo(game, main_model);
cloneTo(game, carousel_model);
visible_count++;
}
}
auto scaleIcons = [&](QStandardItemModel* model) {
for (int i = 0; i < model->rowCount(); i++) {
QStandardItem* item = model->item(i);
QVariant icon_data = item->data(Qt::DecorationRole);
if (icon_data.isValid() && icon_data.canConvert<QPixmap>()) {
QPixmap pixmap = icon_data.value<QPixmap>();
if (!pixmap.isNull()) {
#ifdef __linux__
QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
item->setData(scaled, Qt::DecorationRole);
#else
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 = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
#endif
item->setData(pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
}
}
}
}
};
scaleIcons(fav_model); scaleIcons(main_model); scaleIcons(carousel_model);
fav_model->setSortRole(GameListItemPath::SortRole); fav_model->sort(0, current_sort_order);
main_model->setSortRole(GameListItemPath::SortRole); main_model->sort(0, current_sort_order);
carousel_model->setSortRole(GameListItemPath::SortRole); // Carousel is usually manual or logic-based, but we sort it for consistency
grid_view->setModels(fav_model, main_model);
carousel_view->setModel(carousel_model);
search_field->setFilterResult(visible_count, total_count);
}
@@ -995,6 +996,7 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
grid_view->view()->setContextMenuPolicy(Qt::CustomContextMenu);
grid_view->favView()->setContextMenuPolicy(Qt::CustomContextMenu);
carousel_view->view()->setContextMenuPolicy(Qt::CustomContextMenu);
// Load the persistent index instantly to provide a console-grade experience
@@ -1003,6 +1005,8 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(grid_view->view(), &QListView::customContextMenuRequested, this,
&GameList::PopupContextMenu);
connect(grid_view->favView(), &QListView::customContextMenuRequested, this,
&GameList::PopupContextMenu);
connect(carousel_view->view(), &QWidget::customContextMenuRequested, this,
&GameList::PopupContextMenu);
@@ -1121,9 +1125,14 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
[this]() { SetViewMode(GameList::ViewMode::Grid); });
btn_carousel_view = new QToolButton(toolbar);
QIcon carousel_icon = QIcon::fromTheme(QStringLiteral("view-presentation"));
if (carousel_icon.isNull())
carousel_icon = QIcon::fromTheme(QStringLiteral("media-playlist-play"));
QIcon carousel_icon(QStringLiteral(":/dist/carousel.svg"));
if (carousel_icon.isNull()) {
carousel_icon = QIcon::fromTheme(QStringLiteral("view-presentation"));
if (carousel_icon.isNull())
carousel_icon = QIcon::fromTheme(QStringLiteral("media-playlist-play"));
if (carousel_icon.isNull())
carousel_icon = style()->standardIcon(QStyle::SP_MediaPlay);
}
btn_carousel_view->setIcon(carousel_icon);
btn_carousel_view->setToolTip(tr("Carousel View"));
btn_carousel_view->setCheckable(true);
@@ -1315,7 +1324,13 @@ GameList::GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
// Controller Settings Button
auto* btn_controller_settings = new QToolButton(toolbar);
btn_controller_settings->setIcon(QIcon::fromTheme(QStringLiteral("input-gaming")));
QIcon ctrl_icon(QStringLiteral(":/dist/controller_navigation.svg"));
if (ctrl_icon.isNull()) {
ctrl_icon = QIcon::fromTheme(QStringLiteral("input-gaming"));
if (ctrl_icon.isNull())
ctrl_icon = style()->standardIcon(QStyle::SP_ComputerIcon);
}
btn_controller_settings->setIcon(ctrl_icon);
btn_controller_settings->setToolTip(tr("Controller Navigation Settings"));
btn_controller_settings->setAutoRaise(true);
btn_controller_settings->setIconSize(QSize(16, 16));
@@ -1668,7 +1683,8 @@ void GameList::AddEntry(const QList<QStandardItem*>& entry_items, const QString&
for (int i = 0; i < parent->rowCount(); ++i) {
auto* existing_path_item = static_cast<GameListItemPath*>(parent->child(i, COLUMN_NAME));
if (existing_path_item &&
(existing_path_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id ||
(existing_path_item->data(GameListItemPath::ProgramIdRole).toULongLong() ==
program_id ||
existing_path_item->data(GameListItemPath::FullPathRole).toString() == new_path)) {
// Update existing row columns
for (int col = 0; col < entry_items.size(); ++col) {
@@ -1732,10 +1748,12 @@ void GameList::RefreshGame(u64 program_id, u64 play_time) {
for (int j = 0; j < folder->rowCount(); ++j) {
auto* path_item = static_cast<GameListItemPath*>(folder->child(j, COLUMN_NAME));
if (path_item && path_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
if (path_item &&
path_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
QStandardItem* play_time_item = folder->child(j, COLUMN_PLAY_TIME);
if (play_time_item) {
play_time_item->setData(static_cast<qulonglong>(play_time), GameListItemPlayTime::PlayTimeRole);
play_time_item->setData(static_cast<qulonglong>(play_time),
GameListItemPlayTime::PlayTimeRole);
play_time_item->setData(false, GameListItem::IsRefreshingRole);
}
return;
@@ -2199,30 +2217,22 @@ void GameList::DonePopulating(const QStringList& watch_list) {
emit ShowList(!IsEmpty());
// Only add the "Add Directory" item to the main model for List View.
// Grid and Carousel use a flat model and explicitly skip this type.
// Deduplicate special rows (Add Directory and Favorites)
bool has_add_dir = false;
bool has_favorites = false;
QStandardItem* fav_folder = nullptr;
for (int i = 0; i < item_model->rowCount(); ++i) {
auto type = item_model->item(i)->data(GameListItem::TypeRole).value<GameListItemType>();
if (type == GameListItemType::AddDir)
has_add_dir = true;
if (type == GameListItemType::Favorites)
has_favorites = true;
if (item_model->item(i)->data(GameListItem::TypeRole).value<GameListItemType>() == GameListItemType::Favorites) {
fav_folder = item_model->item(i);
break;
}
}
if (!has_add_dir) {
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
}
if (!has_favorites) {
item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
}
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
UISettings::values.favorited_ids.size() == 0);
tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(),
UISettings::values.favorites_expanded.GetValue());
for (const auto id : UISettings::values.favorited_ids) {
AddFavorite(id);
if (!fav_folder) {
fav_folder = new GameListFavorites();
item_model->invisibleRootItem()->insertRow(0, fav_folder);
} else {
fav_folder->removeRows(0, fav_folder->rowCount());
}
tree_view->setRowHidden(fav_folder->row(), item_model->invisibleRootItem()->index(), UISettings::values.favorited_ids.size() == 0);
tree_view->setExpanded(fav_folder->index(), UISettings::values.favorites_expanded.GetValue());
for (const auto id : UISettings::values.favorited_ids) AddFavorite(id);
auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) {
watcher->removePaths(watch_dirs);
@@ -2281,7 +2291,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
if (current == 0) {
item = tree_view->indexAt(menu_location);
} else if (current == 1) {
item = grid_view->indexAt(menu_location);
QListView* sending_view = qobject_cast<QListView*>(sender());
item = sending_view ? sending_view->indexAt(menu_location)
: grid_view->view()->indexAt(menu_location);
} else {
item = carousel_view->view()->indexAt(menu_location);
}
@@ -2306,27 +2318,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
"QMenu::item:selected { background-color: #32323a; border: 1px solid #42424a; }"
"QMenu::separator { height: 1px; background: #32323a; margin: 4px 10px; }"));
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::Favorites:
case GameListItemType::Game: {
const u64 program_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong();
const std::string path =
selected.data(GameListItemPath::FullPathRole).toString().toStdString();
const auto game_name = selected.data(GameListItemPath::OriginalTitleRole).toString();
AddGamePopup(context_menu, selected, program_id, path, game_name);
break;
if (program_id != 0 || !path.empty()) {
AddGamePopup(context_menu, selected, program_id, path, game_name);
break;
}
[[fallthrough]];
}
case GameListItemType::CustomDir:
AddPermDirPopup(context_menu, selected);
AddCustomDirPopup(context_menu, selected,
false); // Pass false to skip adding "Show Hidden Games"
AddCustomDirPopup(context_menu, selected, false);
break;
case GameListItemType::SdmcDir:
case GameListItemType::UserNandDir:
case GameListItemType::SysNandDir:
AddPermDirPopup(context_menu, selected);
break;
case GameListItemType::Favorites:
AddFavoritesPopup(context_menu);
break;
default:
break;
}
@@ -2334,7 +2346,10 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
if (current == 0) {
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
} else if (current == 1) {
context_menu.exec(grid_view->view()->viewport()->mapToGlobal(menu_location));
QListView* exec_view = qobject_cast<QListView*>(sender());
QPoint global_pt = exec_view ? exec_view->viewport()->mapToGlobal(menu_location)
: grid_view->view()->viewport()->mapToGlobal(menu_location);
context_menu.exec(global_pt);
} else {
context_menu.exec(carousel_view->view()->viewport()->mapToGlobal(menu_location));
}
@@ -2848,11 +2863,14 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
void GameList::AddFavoritesPopup(QMenu& context_menu) {
QAction* clear = context_menu.addAction(tr("Clear"));
connect(clear, &QAction::triggered, [this] {
for (const auto id : UISettings::values.favorited_ids) {
QList<u64> ids_to_remove(UISettings::values.favorited_ids.begin(),
UISettings::values.favorited_ids.end());
for (const auto id : ids_to_remove) {
RemoveFavorite(id);
}
UISettings::values.favorited_ids.clear();
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
FilterGridView(search_field->filterText());
});
}
@@ -2957,7 +2975,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs, bool is_sm
}
auto* delegate = qobject_cast<GameListDelegate*>(tree_view->itemDelegate());
if (delegate) {
delegate->SetPopulating(true);
delegate->SetPopulating(!is_smart_update);
}
if (!is_smart_update) {
@@ -3098,8 +3116,17 @@ void GameList::ToggleFavorite(u64 program_id) {
}
void GameList::AddFavorite(u64 program_id) {
auto* favorites_row = item_model->item(0);
for (int i = 1; i < item_model->rowCount() - 1; i++) {
QStandardItem* favorites_row = nullptr;
for (int i = 0; i < item_model->rowCount(); ++i) {
auto* folder = item_model->item(i, 0);
if (folder && folder->data(GameListItem::TypeRole).value<GameListItemType>() == GameListItemType::Favorites) {
favorites_row = folder;
break;
}
}
if (!favorites_row) return;
for (int i = 0; i < item_model->rowCount(); i++) {
const auto* folder = item_model->item(i);
for (int j = 0; j < folder->rowCount(); j++) {
if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+3 -3
View File
@@ -1148,7 +1148,7 @@ void GMainWindow::InitializeWidgets() {
const QString accent_dim = accent_color.darker(120).name();
unified_top_bar_layout = new QHBoxLayout(unified_top_bar);
unified_top_bar_layout->setContentsMargins(10, 0, 10, 0);
unified_top_bar_layout->setContentsMargins(10, 4, 10, 4);
unified_top_bar_layout->setSpacing(0);
auto add_menu = [this, accent_str](QMenu* menu) {
@@ -1170,9 +1170,9 @@ void GMainWindow::InitializeWidgets() {
btn->setFlat(true);
btn->setFocusPolicy(Qt::NoFocus);
btn->setCursor(Qt::PointingHandCursor);
btn->setFixedHeight(32);
btn->setFixedHeight(42);
btn->setStyleSheet(QString::asprintf(
"QPushButton { border: none; padding: 0 10px; font-weight: normal; font-size: 8.5pt; "
"QPushButton { border: none; padding: 0 14px; font-weight: normal; font-size: 9pt; "
"background: transparent; color: #e0e0e4; text-align: center; margin: 0; outline: none; }"
"QPushButton:hover { background: rgba(255, 255, 255, 0.05); color: #ffffff; "
"border-bottom: 2px solid %s; }"
+68 -22
View File
@@ -12,6 +12,7 @@
#include <QTimer>
#include "citron/ui/game_carousel_view.h"
#include "citron/game_list_p.h"
#include "citron/uisettings.h"
#include "citron/game_list_p.h"
@@ -100,7 +101,12 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
if (!m_model || m_model->rowCount() == 0) return;
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 qreal bs = is + 35.0; const qreal th = is * 2.0;
const int is = UISettings::values.game_icon_size.GetValue();
const float scale = static_cast<float>(is) / 128.0f;
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));
QVector<int> order;
for (int i = 0; i < count; ++i) order << i;
std::sort(order.begin(), order.end(), [this](int a, int b) { return std::abs(a - m_focal_index) > std::abs(b - m_focal_index); });
@@ -111,31 +117,56 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
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);
const int cs_w = is + 25; const int cs_h = is + 25;
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);
QPainterPath path; path.addRoundedRect(cr, 16, 16);
QPainterPath path; path.addRoundedRect(cr, 16 * scale, 16 * scale);
const bool focal = std::abs(i - m_focal_index) < 0.5;
if (focal) {
p.save(); QColor acc = AccentColor(); qreal pulse = (std::sin(m_pulse_tick * 0.1) + 1.0) / 2.0;
if (m_has_focus) { p.setPen(QPen(acc, 4.5 + pulse * 1.5)); acc.setAlphaF(static_cast<float>(0.12 + pulse * 0.08)); p.setBrush(acc); }
else { acc.setAlphaF(0.4f); p.setPen(QPen(acc, 3.0)); p.setBrush(Qt::NoBrush); }
if (m_has_focus) { p.setPen(QPen(acc, (4.5f + pulse * 1.5f) * scale)); acc.setAlphaF(static_cast<float>(0.12 + pulse * 0.08)); p.setBrush(acc); }
else { acc.setAlphaF(0.4f); p.setPen(QPen(acc, 3.0f * scale)); p.setBrush(Qt::NoBrush); }
p.drawPath(path); p.restore();
// Draw the alphabetical header ONLY when it changes (category boundary)
// 1. Determine Section Boundary & Header Drawing
bool draw_header = (i == 0);
int type = m_model->index(i, 0).data(GameListItem::TypeRole).toInt();
int prev_type = (i > 0) ? m_model->index(i - 1, 0).data(GameListItem::TypeRole).toInt() : -1;
bool is_fav = (type == static_cast<int>(GameListItemType::Favorites));
bool was_fav = (prev_type == static_cast<int>(GameListItemType::Favorites));
if (i > 0) {
QString t1 = m_model->index(i, 0).data(Qt::DisplayRole).toString();
QString t2 = m_model->index(i - 1, 0).data(Qt::DisplayRole).toString();
if (t1.isEmpty() || t2.isEmpty() || t1[0].toUpper() != t2[0].toUpper()) draw_header = true;
if (is_fav != was_fav) {
draw_header = true;
} else if (!is_fav) {
QString t1 = m_model->index(i, 0).data(Qt::DisplayRole).toString();
QString t2 = m_model->index(i - 1, 0).data(Qt::DisplayRole).toString();
if (t1.isEmpty() || t2.isEmpty() || t1[0].toUpper() != t2[0].toUpper()) draw_header = true;
}
}
if (draw_header) {
p.save(); p.resetTransform(); p.setPen(acc); p.setOpacity(0.9);
qreal ay = std::max(90.0, y - (s * (cs_h / 2.0)) - 42);
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.0); p.setFont(hf); p.setPen(acc);
p.drawText(QRectF(x - 80, ay - 130, 160, 100), Qt::AlignCenter, cl); p.restore();
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"));
} 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);
}
p.restore();
}
if (i > 0 && was_fav && !is_fav) {
p.save(); p.setOpacity(0.4); p.setPen(QPen(acc, 2.5f * scale));
qreal dx_line = -bs / 2.0;
p.drawLine(QPointF(dx_line, -cs_h / 1.5), QPointF(dx_line, cs_h / 1.5));
p.restore();
}
}
if (!focal) { p.setPen(Qt::NoPen); p.setBrush(CardBg()); p.setOpacity(0.85); p.drawPath(path); }
@@ -152,10 +183,12 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
p.restore();
}
// Static Down Arrow Indicator (ALWAYS centered at the top)
p.save(); QColor acc = AccentColor(); p.setPen(acc); p.setOpacity(0.9);
qreal ay = std::max(55.0, vcy - (1.4 * ((is + 60) / 2.0)) - 28);
QPainterPath static_arr; static_arr.moveTo(vcx, ay); static_arr.lineTo(vcx - 12, ay - 12); static_arr.lineTo(vcx + 12, ay - 12); static_arr.closeSubpath();
qreal arr_size = 12.0f * scale;
QPainterPath static_arr; static_arr.moveTo(vcx, arrow_ay);
static_arr.lineTo(vcx - arr_size, arrow_ay - arr_size);
static_arr.lineTo(vcx + arr_size, arrow_ay - arr_size);
static_arr.closeSubpath();
p.drawPath(static_arr); p.restore();
const int aw = 60, ah = 60;
auto drawArrow = [&](bool left, bool hover) {
@@ -176,7 +209,9 @@ void CinematicCarousel::paintEvent(QPaintEvent* event) {
void CinematicCarousel::mousePressEvent(QMouseEvent* event) {
if (m_left_arrow_hover || m_right_arrow_hover) { m_scroll_timer->start(); return; }
if (m_snap_animation->state() == QAbstractAnimation::Running) m_snap_animation->stop();
m_last_mouse_pos = event->pos(); m_drag_start_pos = event->pos(); m_is_dragging = true;
if (event->button() == Qt::LeftButton) {
m_last_mouse_pos = event->pos(); m_drag_start_pos = event->pos(); m_is_dragging = true;
}
}
void CinematicCarousel::mouseMoveEvent(QMouseEvent* event) {
@@ -226,14 +261,21 @@ void CinematicCarousel::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_X || event->key() == Qt::Key_Y) {
QModelIndex cur = currentIndex();
if (cur.isValid()) {
int type = cur.data(GameListItem::TypeRole).toInt();
QString title = cur.data(Qt::DisplayRole).toString();
QChar cc = title.isEmpty() ? QLatin1Char(' ') : title[0].toUpper();
int tot = m_model->rowCount(); int sr = cur.row();
for (int i = 1; i <= tot; ++i) {
int nr = (sr + i) % tot;
QString nt = m_model->index(nr, 0).data(Qt::DisplayRole).toString();
QChar nc = nt.isEmpty() ? QLatin1Char(' ') : nt[0].toUpper();
if (nc != cc) { scrollTo(nr); return; }
QModelIndex nidx = m_model->index(nr, 0);
int nt = nidx.data(GameListItem::TypeRole).toInt();
if (nt != type) { scrollTo(nr); return; } // Section jump
if (nt != static_cast<int>(GameListItemType::Favorites)) {
QString ntit = nidx.data(Qt::DisplayRole).toString();
QChar nc = ntit.isEmpty() ? QLatin1Char(' ') : ntit[0].toUpper();
if (nc != cc) { scrollTo(nr); return; } // Alpha jump
}
}
}
}
@@ -248,6 +290,10 @@ void CinematicCarousel::startSnapAnimation(qreal target) { m_snap_animation->sto
void CinematicCarousel::updateFocalItem() { if (!m_model) return; int idx = std::round(m_focal_index); if (idx >= 0 && idx < m_model->rowCount()) emit focalItemChanged(m_model->index(idx, 0)); }
void CinematicCarousel::focusOutEvent(QFocusEvent* event) { m_is_dragging = false; m_scroll_timer->stop(); update(); QWidget::focusOutEvent(event); }
void CinematicCarousel::leaveEvent(QEvent* event) { m_is_dragging = false; m_left_arrow_hover = false; m_right_arrow_hover = false; m_hover_icon_index = -1; update(); QWidget::leaveEvent(event); }
QColor CinematicCarousel::CardBg() const { return QColor(25, 25, 28, 205); }
QColor CinematicCarousel::TextColor() const { return QColor(255, 255, 255); }
QColor CinematicCarousel::AccentColor() const { const QString h = QString::fromStdString(UISettings::values.accent_color.GetValue()); return QColor(h).isValid() ? QColor(h) : QColor(0, 150, 255); }
+3
View File
@@ -52,6 +52,8 @@ protected:
void wheelEvent(QWheelEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void focusOutEvent(QFocusEvent* event) override;
void leaveEvent(QEvent* event) override;
private:
void startSnapAnimation(qreal target);
@@ -91,6 +93,7 @@ public:
void ApplyTheme();
CinematicCarousel* view() const { return m_carousel; }
QAbstractItemModel* model() const { return m_carousel->model(); }
void setControllerFocus(bool focus) { m_carousel->setControllerFocus(focus); }
bool hasControllerFocus() const { return m_carousel->hasControllerFocus(); }
+224 -129
View File
@@ -1,188 +1,230 @@
#include <QResizeEvent>
#include <QScrollBar>
#include <QKeyEvent>
#include <QVBoxLayout>
#include <QWheelEvent>
#include <QAbstractItemModel>
#include <QLabel>
#include <QItemSelectionModel>
#include <QScrollArea>
#include "citron/ui/game_grid_view.h"
#include "citron/game_grid_delegate.h"
#include "citron/game_list_p.h"
#include "citron/uisettings.h"
class ContentHeightListView : public QListView {
public:
using QListView::QListView;
protected:
void wheelEvent(QWheelEvent* e) override { e->ignore(); }
};
GameGridView::GameGridView(QWidget* parent) : QWidget(parent) {
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(30, 20, 30, 5); // Tighter bottom margin
m_scroll_area = new QScrollArea(this);
m_scroll_area->setWidgetResizable(true);
m_scroll_area->setFrameShape(QFrame::NoFrame);
m_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scroll_area->setStyleSheet(QStringLiteral("QScrollArea { background: transparent; }"));
m_container = new QWidget(m_scroll_area);
m_layout = new QVBoxLayout(m_container);
m_layout->setContentsMargins(0, 20, 0, 20);
m_layout->setSpacing(0);
// --- TOP HELP OVERLAY ---
m_top_help = new QLabel(this);
m_top_help = new QLabel(m_container);
m_top_help->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_help->setStyleSheet(QStringLiteral("QLabel { color: rgba(255, 255, 255, 140); font-weight: bold; font-family: 'Outfit', 'Inter', sans-serif; font-size: 14px; }"));
m_top_help->setAlignment(Qt::AlignCenter);
m_layout->addSpacing(10);
m_layout->addWidget(m_top_help);
m_layout->addSpacing(30);
m_layout->addSpacing(20);
m_view = new QListView(this);
m_view->setViewMode(QListView::IconMode);
m_view->setResizeMode(QListView::Adjust);
m_view->setFlow(QListView::LeftToRight);
m_view->setWrapping(true);
m_view->setSpacing(20);
m_view->setFrameStyle(QFrame::NoFrame);
m_view->setAttribute(Qt::WA_MacShowFocusRect, false);
// Modernized ScrollBar Styling
m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto setupView = [&](QListView*& view, GameGridDelegate*& delegate) {
view = new ContentHeightListView(m_container);
view->setViewMode(QListView::IconMode);
view->setResizeMode(QListView::Adjust);
view->setFlow(QListView::LeftToRight);
view->setWrapping(true);
view->setFrameStyle(QFrame::NoFrame);
view->setAttribute(Qt::WA_MacShowFocusRect, false);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setDragEnabled(false);
view->setAcceptDrops(false);
view->setFocusPolicy(Qt::StrongFocus);
delegate = new GameGridDelegate(view, this);
view->setItemDelegate(delegate);
connect(view, &QListView::activated, this, &GameGridView::itemActivated);
};
// Disable traditional Drag & Drop to ensure Kinetic Scroll takes precedence
m_view->setDragEnabled(false);
m_view->setAcceptDrops(false);
m_view->setDropIndicatorShown(false);
m_view->setDefaultDropAction(Qt::IgnoreAction);
m_fav_label = new QLabel(tr("★ FAVORITES"), m_container);
m_fav_label->setStyleSheet(QStringLiteral("QLabel { color: #ffd700; font-weight: bold; font-size: 18px; padding-left: 24px; margin-bottom: 10px; }"));
m_layout->addWidget(m_fav_label);
setupView(m_fav_view, m_fav_delegate);
m_layout->addWidget(m_fav_view);
m_delegate = new GameGridDelegate(m_view, this);
m_view->setItemDelegate(m_delegate);
m_main_label = new QLabel(tr("ALL GAMES"), m_container);
m_main_label->setStyleSheet(QStringLiteral("QLabel { color: #0096ff; font-weight: bold; font-size: 18px; padding-left: 24px; margin-top: 20px; margin-bottom: 10px; }"));
m_layout->addWidget(m_main_label);
setupView(m_main_view, m_main_delegate);
m_layout->addWidget(m_main_view);
m_layout->addWidget(m_view, 1);
// --- BOTTOM HINT OVERLAY ---
m_bottom_hint = new QLabel(this);
m_bottom_hint = new QLabel(m_container);
m_bottom_hint->setText(tr("*You can use your Mouse Wheel or the Scrollbar to navigate the Grid View*"));
m_bottom_hint->setStyleSheet(QStringLiteral("QLabel { color: rgba(255, 255, 255, 100); font-style: italic; font-size: 13px; }"));
m_bottom_hint->setAlignment(Qt::AlignCenter);
m_layout->addWidget(m_bottom_hint);
m_scroll_area->setWidget(m_container);
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->addWidget(m_scroll_area);
ApplyTheme();
connect(m_view, &QListView::activated, this, &GameGridView::itemActivated);
}
void GameGridView::ApplyTheme() {
m_view->setStyleSheet(QStringLiteral(
"QListView {"
" background: transparent;"
" border: none;"
" outline: 0;"
" padding: 20px;"
"}"
"QListView::item {"
" padding: 0px;"
" margin: 4px;"
" background: transparent;"
"}"
));
QString style = QStringLiteral(
"QListView { background: transparent; border: none; outline: 0; padding: 0px; }"
"QListView::item { padding: 0px; margin: 0px; background: transparent; }"
);
m_fav_view->setStyleSheet(style);
m_main_view->setStyleSheet(style);
}
QAbstractItemModel* GameGridView::model() const {
return m_view->model();
void GameGridView::setModels(QAbstractItemModel* fav_model, QAbstractItemModel* main_model) {
m_fav_view->setModel(fav_model);
m_main_view->setModel(main_model);
bool has_favs = fav_model && fav_model->rowCount() > 0;
m_fav_label->setVisible(has_favs);
m_fav_view->setVisible(has_favs);
if (m_fav_view->selectionModel()) {
connect(m_fav_view->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex& current) {
if (current.isValid()) {
emit itemSelectionChanged(current);
m_main_view->clearSelection();
m_main_view->setCurrentIndex(QModelIndex());
}
});
}
if (m_main_view->selectionModel()) {
connect(m_main_view->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex& current) {
if (current.isValid()) {
emit itemSelectionChanged(current);
m_fav_view->clearSelection();
m_fav_view->setCurrentIndex(QModelIndex());
}
});
}
UpdateGridSize();
}
QItemSelectionModel* GameGridView::selectionModel() const {
return m_view->selectionModel();
QModelIndex GameGridView::currentIndex() const {
if (m_fav_view->hasFocus()) return m_fav_view->currentIndex();
return m_main_view->currentIndex();
}
QRect GameGridView::visualRect(const QModelIndex& index) const {
return m_view->visualRect(index);
void GameGridView::setCurrentIndex(const QModelIndex& index) {
if (index.model() == m_fav_view->model()) m_fav_view->setCurrentIndex(index);
else m_main_view->setCurrentIndex(index);
}
QWidget* GameGridView::viewport() const {
return m_view->viewport();
}
void GameGridView::scrollTo(const QModelIndex& index) {
m_view->scrollTo(index);
QModelIndex GameGridView::indexAt(const QPoint& p) const {
QPoint fp = m_fav_view->mapFrom(this, p);
if (m_fav_view->rect().contains(fp)) return m_fav_view->indexAt(fp);
QPoint mp = m_main_view->mapFrom(this, p);
if (m_main_view->rect().contains(mp)) return m_main_view->indexAt(mp);
return QModelIndex();
}
void GameGridView::setControllerFocus(bool focus) {
m_has_focus = focus;
if (focus) {
m_view->setFocus();
if (!m_view->currentIndex().isValid() && m_view->model() && m_view->model()->rowCount() > 0) {
m_view->setCurrentIndex(m_view->model()->index(0, 0));
if (m_fav_view->isVisible() && m_fav_view->model()->rowCount() > 0) {
m_fav_view->setFocus();
if (!m_fav_view->currentIndex().isValid()) m_fav_view->setCurrentIndex(m_fav_view->model()->index(0, 0));
} else {
m_main_view->setFocus();
if (!m_main_view->currentIndex().isValid()) m_main_view->setCurrentIndex(m_main_view->model()->index(0, 0));
}
}
}
void GameGridView::onNavigated(int dx, int dy) {
if (!m_has_focus || !m_view->model()) return;
QModelIndex current = m_view->currentIndex();
if (!current.isValid()) {
m_view->setCurrentIndex(m_view->model()->index(0, 0));
if (!m_has_focus) return;
QListView* current = m_fav_view->hasFocus() ? m_fav_view : m_main_view;
if (!current->model()) return;
QModelIndex idx = current->currentIndex();
if (!idx.isValid()) {
current->setCurrentIndex(current->model()->index(0, 0));
return;
}
int row = idx.row();
int total = current->model()->rowCount();
int cols = qMax(1, current->viewport()->width() / current->gridSize().width());
if (dy > 0 && row + cols >= total && current == m_fav_view) {
m_main_view->setFocus();
m_main_view->setCurrentIndex(m_main_view->model()->index(0, 0));
return;
}
if (dy < 0 && row < cols && current == m_main_view && m_fav_view->isVisible()) {
m_fav_view->setFocus();
m_fav_view->setCurrentIndex(m_fav_view->model()->index(m_fav_view->model()->rowCount() - 1, 0));
return;
}
// Grid navigation logic (simplified for now, can be improved)
int row = current.row();
int total = m_view->model()->rowCount();
if (dx > 0) row++;
else if (dx < 0) row--;
if (dy > 0) {
// Find how many items per row
int icons_per_row = qMax(1, m_view->viewport()->width() / m_view->gridSize().width());
row += icons_per_row;
} else if (dy < 0) {
int icons_per_row = qMax(1, m_view->viewport()->width() / m_view->gridSize().width());
row -= icons_per_row;
}
if (dy > 0) row += cols;
else if (dy < 0) row -= cols;
row = qBound(0, row, total - 1);
m_view->setCurrentIndex(m_view->model()->index(row, 0));
m_view->scrollTo(m_view->currentIndex());
current->setCurrentIndex(current->model()->index(row, 0));
scrollTo(current->currentIndex());
}
void GameGridView::onActivated() {
if (!m_has_focus) return;
QModelIndex current = m_view->currentIndex();
if (current.isValid()) {
emit itemActivated(current);
}
QModelIndex cur = currentIndex();
if (cur.isValid()) emit itemActivated(cur);
}
void GameGridView::onCancelled() {
if (!m_has_focus) return;
m_has_focus = false;
m_view->clearFocus();
}
void GameGridView::setModel(QAbstractItemModel* model) {
m_view->setModel(model);
if (m_view->selectionModel()) {
connect(m_view->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex& current) {
emit itemSelectionChanged(current);
});
}
m_fav_view->clearFocus();
m_main_view->clearFocus();
}
void GameGridView::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_X || event->key() == Qt::Key_Y) {
if (!m_view->model()) return;
QModelIndex current = m_view->currentIndex();
if (!current.isValid()) {
if (m_view->model()->rowCount() > 0) {
m_view->setCurrentIndex(m_view->model()->index(0, 0));
return;
}
QListView* current = m_fav_view->hasFocus() ? m_fav_view : m_main_view;
if (current == m_fav_view && m_main_view->model()->rowCount() > 0) {
m_main_view->setFocus();
m_main_view->setCurrentIndex(m_main_view->model()->index(0, 0));
return;
}
QString title = current.data(Qt::DisplayRole).toString();
QChar current_char = title.isEmpty() ? QLatin1Char(' ') : title[0].toUpper();
int total = m_view->model()->rowCount();
int start_row = current.row();
for (int i = 1; i <= total; ++i) {
int next_row = (start_row + i) % total;
QString next_title = m_view->model()->index(next_row, 0).data(Qt::DisplayRole).toString();
QChar next_char = next_title.isEmpty() ? QLatin1Char(' ') : next_title[0].toUpper();
if (next_char != current_char) {
m_view->setCurrentIndex(m_view->model()->index(next_row, 0));
m_view->scrollTo(m_view->model()->index(next_row, 0));
if (!current->model()) return;
QModelIndex cur = current->currentIndex();
if (!cur.isValid()) return;
QString title = cur.data(Qt::DisplayRole).toString();
QChar cc = title.isEmpty() ? QLatin1Char(' ') : title[0].toUpper();
int tot = current->model()->rowCount();
int sr = cur.row();
for (int i = 1; i <= tot; ++i) {
int nr = (sr + i) % tot;
QModelIndex nidx = current->model()->index(nr, 0);
QString nt = nidx.data(Qt::DisplayRole).toString();
QChar nc = nt.isEmpty() ? QLatin1Char(' ') : nt[0].toUpper();
if (nc != cc) {
current->setCurrentIndex(nidx);
current->scrollTo(nidx);
return;
}
}
@@ -191,15 +233,68 @@ void GameGridView::keyPressEvent(QKeyEvent* event) {
void GameGridView::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event);
// Adaptive Grid Scaling: Eliminate the Gap
const int icon_size = UISettings::values.game_icon_size.GetValue();
const int base_width = icon_size + 40;
const int total_width = m_view->viewport()->width() - 30; // margins
if (total_width > base_width) {
int columns = qMax(1, total_width / base_width);
int adaptive_width = total_width / columns;
m_view->setGridSize(QSize(adaptive_width, base_width + 45));
}
UpdateGridSize();
}
void GameGridView::UpdateGridSize() {
const int is = UISettings::values.game_icon_size.GetValue();
const float s = static_cast<float>(is) / 128.0f;
const int bw = qMax(is + static_cast<int>(40 * s), is + 24);
const int tw = m_scroll_area->viewport()->width();
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);
m_fav_view->setGridSize(gs);
m_main_view->setGridSize(gs);
m_fav_view->setFixedWidth(tw);
m_main_view->setFixedWidth(tw);
}
UpdateLayoutHeights();
}
void GameGridView::UpdateLayoutHeights() {
auto updateHeight = [&](QListView* view) {
if (!view || !view->isVisible() || !view->model() || view->model()->rowCount() == 0) {
view->setFixedHeight(0);
return;
}
const int count = view->model()->rowCount();
const QSize gs = view->gridSize();
if (gs.width() <= 0 || gs.height() <= 0) return;
const int tw = m_scroll_area->viewport()->width();
const int cols = qMax(1, tw / gs.width());
const int rows = (count + cols - 1) / cols;
view->setFixedHeight(rows * gs.height() + 40);
};
updateHeight(m_fav_view);
updateHeight(m_main_view);
}
QAbstractItemModel* GameGridView::favModel() const { return m_fav_view->model(); }
QAbstractItemModel* GameGridView::mainModel() const { return m_main_view->model(); }
QItemSelectionModel* GameGridView::selectionModel() const {
return m_main_view->selectionModel();
}
void GameGridView::setModel(QAbstractItemModel* model) {
m_main_view->setModel(model);
m_fav_view->setModel(nullptr);
m_fav_label->hide();
m_fav_view->hide();
UpdateGridSize();
}
QRect GameGridView::visualRect(const QModelIndex& index) const {
if (index.model() == m_fav_view->model()) return m_fav_view->visualRect(index);
return m_main_view->visualRect(index);
}
QWidget* GameGridView::viewport() const { return m_scroll_area->viewport(); }
void GameGridView::scrollTo(const QModelIndex& index) {
QListView* view = (index.model() == m_fav_view->model()) ? m_fav_view : m_main_view;
QRect rect = view->visualRect(index);
rect.moveTop(rect.top() + view->y());
m_scroll_area->ensureVisible(rect.center().x(), rect.center().y(), 50, 50);
}
+25 -11
View File
@@ -1,5 +1,6 @@
#pragma once
#include <QWidget>
#include <QListView>
#include <QVBoxLayout>
#include <QLabel>
@@ -19,23 +20,26 @@ class GameGridView : public QWidget {
public:
explicit GameGridView(QWidget* parent = nullptr);
void setModel(QAbstractItemModel* model);
void setModels(QAbstractItemModel* fav_model, QAbstractItemModel* main_model);
void ApplyTheme();
// Accessors for compatibility
QListView* view() const { return m_view; }
QAbstractItemModel* model() const;
QListView* view() const { return m_main_view; }
QListView* favView() const { return m_fav_view; }
QAbstractItemModel* model() const { return mainModel(); }
QItemSelectionModel* selectionModel() const;
void setModel(QAbstractItemModel* model);
QAbstractItemModel* favModel() const;
QAbstractItemModel* mainModel() const;
QRect visualRect(const QModelIndex& index) const;
QWidget* viewport() const;
void scrollTo(const QModelIndex& index);
// Forwarded from GameViewBase equivalents
QModelIndex currentIndex() const { return m_view->currentIndex(); }
void setCurrentIndex(const QModelIndex& index) { m_view->setCurrentIndex(index); }
QModelIndex indexAt(const QPoint& p) const { return m_view->indexAt(p); }
QModelIndex currentIndex() const;
void setCurrentIndex(const QModelIndex& index);
QModelIndex indexAt(const QPoint& p) const;
// Controller Navigation
void setControllerFocus(bool focus);
bool hasControllerFocus() const { return m_has_focus; }
@@ -53,10 +57,20 @@ protected:
void resizeEvent(QResizeEvent* event) override;
private:
QListView* m_view = nullptr;
GameGridDelegate* m_delegate = nullptr;
void UpdateGridSize();
void UpdateLayoutHeights();
QScrollArea* m_scroll_area = nullptr;
QWidget* m_container = nullptr;
QListView* m_fav_view = nullptr;
QListView* m_main_view = nullptr;
QLabel* m_fav_label = nullptr;
QLabel* m_main_label = nullptr;
GameGridDelegate* m_fav_delegate = nullptr;
GameGridDelegate* m_main_delegate = nullptr;
QVBoxLayout* m_layout = nullptr;
QLabel* m_top_help = nullptr;
QLabel* m_bottom_hint = nullptr;
bool m_has_focus = false;
};