Browse Source

[desktop] Basic grid view implementation (#3479)

Closes #3441

Basic impl of a grid view on the game list. The ideal solution here
would be to use QSortFilterProxyModel and abstract the game list model
out to a QStandardItemModel, but that is too much effort for me rn.
Adapted the "card" design from QML, can 1000% be improved but QPainter
is just such a pain to deal with. Implanting a Qt Quick scene into there
would legitimately be easier.

Anyways, margins and text sizes lgtm at all sizes, though please give
feedback on both that and the general card design.

Future TODOs:
- [ ] Auto size mode
- [ ] Refactor to use models

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3479
revert-noinline
crueter 1 day ago
parent
commit
b9e052b3a7
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 1
      src/common/settings_enums.h
  2. 6
      src/qt_common/config/shared_translation.cpp
  3. 4
      src/qt_common/config/uisettings.h
  4. 15
      src/yuzu/CMakeLists.txt
  5. 104
      src/yuzu/game/game_card.cpp
  6. 27
      src/yuzu/game/game_card.h
  7. 326
      src/yuzu/game/game_list.cpp
  8. 17
      src/yuzu/game/game_list.h
  9. 45
      src/yuzu/game/game_list_p.h
  10. 13
      src/yuzu/game/game_list_worker.cpp
  11. 2
      src/yuzu/game/game_list_worker.h
  12. 24
      src/yuzu/main.ui
  13. 25
      src/yuzu/main_window.cpp
  14. 6
      src/yuzu/main_window.h
  15. 4
      src/yuzu/multiplayer/chat_room.cpp
  16. 4
      src/yuzu/multiplayer/client_room.cpp
  17. 4
      src/yuzu/multiplayer/host_room.cpp
  18. 4
      src/yuzu/multiplayer/lobby.cpp
  19. 4
      src/yuzu/multiplayer/state.cpp

1
src/common/settings_enums.h

@ -155,6 +155,7 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(GameListMode, TreeView, GridView);
template <typename Type>
inline std::string_view CanonicalizeEnum(Type id) {

6
src/qt_common/config/shared_translation.cpp

@ -766,6 +766,12 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent)
PAIR(ExtendedDynamicState, EDS3, tr("ExtendedDynamicState 3")),
}});
translations->insert({Settings::EnumMetadata<Settings::GameListMode>::Index(),
{
PAIR(GameListMode, TreeView, tr("Tree View")),
PAIR(GameListMode, GridView, tr("Grid View")),
}});
#undef PAIR
#undef CTX_PAIR

4
src/qt_common/config/uisettings.h

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
@ -210,6 +210,8 @@ struct Values {
Setting<u32> folder_icon_size{linkage, 48, "folder_icon_size", Category::UiGameList};
Setting<u8> row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList};
Setting<u8> row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList};
Setting<Settings::GameListMode> game_list_mode{linkage, Settings::GameListMode::TreeView, "game_list_mode", Category::UiGameList};
std::atomic_bool is_game_list_reload_pending{false};
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};

15
src/yuzu/CMakeLists.txt

@ -156,11 +156,14 @@ add_executable(yuzu
debugger/controller.cpp
debugger/controller.h
game_list.cpp
game_list.h
game_list_p.h
game_list_worker.cpp
game_list_worker.h
game/game_list.cpp
game/game_list.h
game/game_list_p.h
game/game_list_worker.cpp
game/game_list_worker.h
game/game_card.h
game/game_card.cpp
hotkeys.cpp
hotkeys.h
install_dialog.cpp
@ -234,7 +237,7 @@ add_executable(yuzu
data_dialog.h data_dialog.cpp data_dialog.ui
data_widget.ui
ryujinx_dialog.h ryujinx_dialog.cpp ryujinx_dialog.ui
main_window.h main_window.cpp
main_window.h main_window.cpp main.ui
configuration/system/new_user_dialog.h configuration/system/new_user_dialog.cpp configuration/system/new_user_dialog.ui
configuration/system/profile_avatar_dialog.h configuration/system/profile_avatar_dialog.cpp

104
src/yuzu/game/game_card.cpp

@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPainter>
#include "game_card.h"
#include "qt_common/config/uisettings.h"
GameCard::GameCard(QObject* parent) : QStyledItemDelegate{parent} {
setObjectName("GameCard");
}
void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const {
if (!index.isValid())
return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
// padding
QRect cardRect = option.rect.adjusted(4, 4, -4, -4);
// colors
QPalette palette = option.palette;
QColor backgroundColor = palette.window().color();
QColor borderColor = palette.dark().color();
QColor textColor = palette.text().color();
// if it's selected add a blue background
if (option.state & QStyle::State_Selected) {
backgroundColor = palette.highlight().color();
borderColor = palette.highlight().color().lighter(150);
textColor = palette.highlightedText().color();
} else if (option.state & QStyle::State_MouseOver) {
backgroundColor = backgroundColor.lighter(110);
}
// bg
painter->setBrush(backgroundColor);
painter->setPen(QPen(borderColor, 1));
painter->drawRoundedRect(cardRect, 10, 10);
static constexpr const int padding = 10;
// icon
int _iconsize = UISettings::values.game_icon_size.GetValue();
QSize iconSize(_iconsize, _iconsize);
QPixmap iconPixmap = index.data(Qt::DecorationRole).value<QPixmap>();
QRect iconRect;
if (!iconPixmap.isNull()) {
QSize scaledSize = iconPixmap.size();
scaledSize.scale(iconSize, Qt::KeepAspectRatio);
int x = cardRect.left() + (cardRect.width() - scaledSize.width()) / 2;
int y = cardRect.top() + padding;
iconRect = QRect(x, y, scaledSize.width(), scaledSize.height());
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
painter->drawPixmap(iconRect, iconPixmap);
} else {
// if there is no icon just draw a blank rect
iconRect = QRect(cardRect.left() + padding,
cardRect.top() + padding,
_iconsize, _iconsize);
}
// if "none" is selected, pretend there's a
_iconsize = _iconsize ? _iconsize : 96;
// padding + text
QRect textRect = cardRect;
textRect.setTop(iconRect.bottom() + 8);
textRect.adjust(padding, 0, -padding, -padding);
// We are already crammed on space, ignore the row 2
QString title = index.data(Qt::DisplayRole).toString();
title = title.split(QLatin1Char('\n')).first();
// now draw text
painter->setPen(textColor);
QFont font = option.font;
font.setBold(true);
// TODO(crueter): fix this abysmal scaling
// If "none" is selected, then default to 8.5 point font.
font.setPointSize(1 + std::max(7.0, _iconsize ? std::sqrt(_iconsize * 0.6) : 7.5));
// TODO(crueter): elide mode
painter->setFont(font);
painter->drawText(textRect, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, title);
painter->restore();
}
QSize GameCard::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const {
return m_size;
}
void GameCard::setSize(const QSize& newSize) {
m_size = newSize;
}

27
src/yuzu/game/game_card.h

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QStyledItemDelegate>
/**
* A stylized "card"-like delegate for the game grid view.
* Adapted from QML
*/
class GameCard : public QStyledItemDelegate {
Q_OBJECT
public:
explicit GameCard(QObject* parent = nullptr);
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setSize(const QSize& newSize);
private:
QSize m_size;
};

326
src/yuzu/game_list.cpp → src/yuzu/game/game_list.cpp

@ -9,29 +9,30 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QList>
#include <QListView>
#include <QMenu>
#include <QScroller>
#include <QScrollBar>
#include <QScroller>
#include <QThreadPool>
#include <QToolButton>
#include <QVariantAnimation>
#include <fmt/ranges.h>
#include <qfilesystemwatcher.h>
#include <qnamespace.h>
#include <qscroller.h>
#include <qscrollerproperties.h>
#include <QAbstractItemView>
#include <QScroller>
#include <QScrollerProperties>
#include "common/common_types.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "game/game_card.h"
#include "qt_common/config/uisettings.h"
#include "qt_common/util/game.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/game_list.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game_list_worker.h"
#include "yuzu/game/game_list.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/game/game_list_worker.h"
#include "yuzu/main_window.h"
#include "yuzu/util/controller_navigation.h"
#include "qt_common/qt_common.h"
@ -198,25 +199,56 @@ void GameList::OnTextChanged(const QString& new_text) {
QString edit_filter_text = new_text.toLower();
QStandardItem* folder;
int children_total = 0;
int result_count = 0;
auto hide = [this](int row, bool hidden, QModelIndex index = QModelIndex()) {
if (m_isTreeMode) {
tree_view->setRowHidden(row, index, hidden);
} else {
list_view->setRowHidden(row, hidden);
}
};
// If the searchfield is empty every item is visible
// Otherwise the filter gets applied
if (edit_filter_text.isEmpty()) {
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
UISettings::values.favorited_ids.size() == 0);
// TODO(crueter) dedupe
if (!m_isTreeMode) {
int row_count = item_model->rowCount();
for (int i = 0; i < row_count; ++i) {
QStandardItem* item = item_model->item(i, 0);
if (!item) continue;
children_total++;
const QString file_path = item->data(GameListItemPath::FullPathRole).toString().toLower();
const QString file_title = item->data(GameListItemPath::TitleRole).toString().toLower();
const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) +
QLatin1Char{' '} + file_title;
if (edit_filter_text.isEmpty() || ContainsAllWords(file_name, edit_filter_text)) {
hide(i, false);
result_count++;
} else {
hide(i, true);
}
}
search_field->setFilterResult(result_count, children_total);
} else if (edit_filter_text.isEmpty()) {
hide(0, UISettings::values.favorited_ids.size() == 0, item_model->invisibleRootItem()->index());
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
folder = item_model->item(i, 0);
const QModelIndex folder_index = folder->index();
const int children_count = folder->rowCount();
for (int j = 0; j < children_count; ++j) {
++children_total;
tree_view->setRowHidden(j, folder_index, false);
hide(j, false, folder_index);
}
}
search_field->setFilterResult(children_total, children_total);
} else {
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
int result_count = 0;
hide(0, true, item_model->invisibleRootItem()->index());
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
folder = item_model->item(i, 0);
const QModelIndex folder_index = folder->index();
@ -245,10 +277,10 @@ void GameList::OnTextChanged(const QString& new_text) {
file_title;
if (ContainsAllWords(file_name, edit_filter_text) ||
(file_program_id.size() == 16 && file_program_id.contains(edit_filter_text))) {
tree_view->setRowHidden(j, folder_index, false);
hide(j, false, folder_index);
++result_count;
} else {
tree_view->setRowHidden(j, folder_index, true);
hide(j, true, folder_index);
}
}
}
@ -334,28 +366,21 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
this->main_window = parent;
layout = new QVBoxLayout;
tree_view = new QTreeView;
tree_view = new QTreeView(this);
list_view = new QListView(this);
m_gameCard = new GameCard(this);
list_view->setItemDelegate(m_gameCard);
controller_navigation = new ControllerNavigation(system.HIDCore(), this);
search_field = new GameListSearchField(this);
item_model = new QStandardItemModel(tree_view);
tree_view->setModel(item_model);
list_view->setModel(item_model);
SetupScrollAnimation();
tree_view->viewport()->installEventFilter(this);
// touch gestures
tree_view->viewport()->grabGesture(Qt::SwipeGesture);
tree_view->viewport()->grabGesture(Qt::PanGesture);
// TODO: touch?
QScroller::grabGesture(tree_view->viewport(), QScroller::LeftMouseButtonGesture);
auto scroller = QScroller::scroller(tree_view->viewport());
QScrollerProperties props;
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
scroller->setScrollerProperties(props);
// tree
tree_view->setAlternatingRowColors(true);
tree_view->setSelectionMode(QHeaderView::SingleSelection);
tree_view->setSelectionBehavior(QHeaderView::SelectRows);
@ -367,6 +392,24 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
tree_view->setAttribute(Qt::WA_AcceptTouchEvents, true);
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
// list view setup
list_view->setViewMode(QListView::IconMode);
list_view->setResizeMode(QListView::Adjust);
list_view->setUniformItemSizes(false);
list_view->setSelectionMode(QAbstractItemView::SingleSelection);
list_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
list_view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
list_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
list_view->setContextMenuPolicy(Qt::CustomContextMenu);
list_view->setGridSize(QSize(140, 160));
m_gameCard->setSize(list_view->gridSize());
list_view->setSpacing(10);
list_view->setWordWrap(true);
list_view->setTextElideMode(Qt::ElideRight);
list_view->setFlow(QListView::LeftToRight);
list_view->setWrapping(true);
item_model->insertColumns(0, COLUMN_COUNT);
RetranslateUI();
@ -376,8 +419,13 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
item_model->setSortRole(GameListItemPath::SortRole);
connect(main_window, &MainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(list_view, &QListView::activated, this, &GameList::ValidateEntry);
connect(list_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded);
connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded);
connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, this,
@ -391,6 +439,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
}
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
QCoreApplication::postEvent(tree_view, event);
QCoreApplication::postEvent(list_view, event);
});
// We must register all custom types with the Qt Automoc system so that we are able to use
@ -401,14 +450,70 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
layout->setSpacing(0);
layout->addWidget(tree_view);
layout->addWidget(list_view);
layout->addWidget(search_field);
setLayout(layout);
ResetViewMode();
}
void GameList::UnloadController() {
controller_navigation->UnloadController();
}
bool GameList::IsTreeMode() {
return m_isTreeMode;
}
void GameList::ResetViewMode() {
auto &setting = UISettings::values.game_list_mode;
bool newTreeMode = false;
switch (setting.GetValue()) {
case Settings::GameListMode::TreeView:
m_currentView = tree_view;
newTreeMode = true;
tree_view->setVisible(true);
list_view->setVisible(false);
break;
case Settings::GameListMode::GridView:
m_currentView = list_view;
newTreeMode = false;
list_view->setVisible(true);
tree_view->setVisible(false);
break;
default:
break;
}
if (m_isTreeMode != newTreeMode) {
m_isTreeMode = newTreeMode;
auto view = m_currentView->viewport();
view->installEventFilter(this);
// touch gestures
view->grabGesture(Qt::SwipeGesture);
view->grabGesture(Qt::PanGesture);
// TODO: touch?
QScroller::grabGesture(view, QScroller::LeftMouseButtonGesture);
auto scroller = QScroller::scroller(view);
QScrollerProperties props;
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy,
QScrollerProperties::OvershootAlwaysOff);
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy,
QScrollerProperties::OvershootAlwaysOff);
scroller->setScrollerProperties(props);
RefreshGameDirectory();
}
}
GameList::~GameList() {
UnloadController();
}
@ -432,14 +537,20 @@ void GameList::WorkerEvent() {
}
void GameList::AddDirEntry(GameListDir* entry_items) {
item_model->invisibleRootItem()->appendRow(entry_items);
tree_view->setExpanded(
entry_items->index(),
UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()].expanded);
if (m_isTreeMode) {
item_model->invisibleRootItem()->appendRow(entry_items);
tree_view->setExpanded(
entry_items->index(),
UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()]
.expanded);
}
}
void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
parent->appendRow(entry_items);
if (!m_isTreeMode)
item_model->invisibleRootItem()->appendRow(entry_items);
else
parent->appendRow(entry_items);
}
void GameList::ValidateEntry(const QModelIndex& item) {
@ -497,16 +608,18 @@ bool GameList::IsEmpty() const {
void GameList::DonePopulating(const QStringList& watch_list) {
emit ShowList(!IsEmpty());
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
// Add favorites row
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 : std::as_const(UISettings::values.favorited_ids)) {
AddFavorite(id);
if (m_isTreeMode) {
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
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 : std::as_const(UISettings::values.favorited_ids)) {
AddFavorite(id);
}
}
// Clear out the old directories to watch for changes and add the new ones
@ -538,7 +651,8 @@ void GameList::DonePopulating(const QStringList& watch_list) {
#ifdef __APPLE__
watcher->blockSignals(old_signals_blocked);
#endif
tree_view->setEnabled(true);
m_currentView->setEnabled(true);
int children_total = 0;
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
children_total += item_model->item(i, 0)->rowCount();
@ -554,9 +668,18 @@ void GameList::DonePopulating(const QStringList& watch_list) {
}
void GameList::PopupContextMenu(const QPoint& menu_location) {
QModelIndex item = tree_view->indexAt(menu_location);
if (!item.isValid())
QModelIndex item = m_currentView->indexAt(menu_location);
if (!item.isValid()) {
if (m_isTreeMode)
return;
QMenu blank_menu;
QAction *addGameDirAction = blank_menu.addAction(tr("&Add New Game Directory"));
connect(addGameDirAction, &QAction::triggered, this, &GameList::AddDirectory);
blank_menu.exec(m_currentView->viewport()->mapToGlobal(menu_location));
return;
}
const auto selected = item.sibling(item.row(), 0);
QMenu context_menu;
@ -580,7 +703,7 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
default:
break;
}
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
context_menu.exec(m_currentView->viewport()->mapToGlobal(menu_location));
}
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
@ -641,7 +764,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
connect(favorite, &QAction::triggered, this, [this, program_id]() { ToggleFavorite(program_id); });
connect(favorite, &QAction::triggered, this,
[this, program_id]() { ToggleFavorite(program_id); });
connect(open_save_location, &QAction::triggered, this, [this, program_id, path]() {
emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path);
});
@ -661,26 +785,32 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::Update);
});
connect(remove_dlc, &QAction::triggered, this, [this, program_id]() {
emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::AddOnContent);
emit RemoveInstalledEntryRequested(program_id,
QtCommon::Game::InstalledEntryType::AddOnContent);
});
connect(remove_gl_shader_cache, &QAction::triggered, this, [this, program_id, path]() {
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::GlShaderCache, path);
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::GlShaderCache,
path);
});
connect(remove_vk_shader_cache, &QAction::triggered, this, [this, program_id, path]() {
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::VkShaderCache, path);
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::VkShaderCache,
path);
});
connect(remove_shader_cache, &QAction::triggered, this, [this, program_id, path]() {
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::AllShaderCache, path);
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::AllShaderCache,
path);
});
connect(remove_custom_config, &QAction::triggered, this, [this, program_id, path]() {
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
emit RemoveFileRequested(program_id,
QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
});
connect(set_play_time, &QAction::triggered, this,
[this, program_id]() { emit SetPlayTimeRequested(program_id); });
connect(remove_play_time_data, &QAction::triggered, this,
[this, program_id]() { emit RemovePlayTimeRequested(program_id); });
connect(remove_cache_storage, &QAction::triggered, this, [this, program_id, path] {
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CacheStorage, path);
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CacheStorage,
path);
});
connect(dump_romfs, &QAction::triggered, this, [this, program_id, path]() {
emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::Normal);
@ -700,15 +830,16 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(create_desktop_shortcut, &QAction::triggered, this, [this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Desktop);
});
connect(create_applications_menu_shortcut, &QAction::triggered, this, [this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Applications);
});
connect(create_applications_menu_shortcut, &QAction::triggered, this,
[this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Applications);
});
#endif
connect(properties, &QAction::triggered, this,
[this, path]() { emit OpenPerGameGeneralRequested(path); });
connect(ryujinx, &QAction::triggered, this, [this, program_id]() { emit LinkToRyujinxRequested(program_id);
});
connect(ryujinx, &QAction::triggered, this,
[this, program_id]() { emit LinkToRyujinxRequested(program_id); });
};
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
@ -816,7 +947,7 @@ void GameList::LoadCompatibilityList() {
const QJsonDocument json = QJsonDocument::fromJson(content);
const QJsonArray arr = json.array();
for (const QJsonValue &value : arr) {
for (const QJsonValue& value : arr) {
const QJsonObject game = value.toObject();
const QString compatibility_key = QStringLiteral("compatibility");
@ -828,7 +959,7 @@ void GameList::LoadCompatibilityList() {
const QString directory = game[QStringLiteral("directory")].toString();
const QJsonArray ids = game[QStringLiteral("releases")].toArray();
for (const QJsonValue &id_ref : ids) {
for (const QJsonValue& id_ref : ids) {
const QJsonObject id_object = id_ref.toObject();
const QString id = id_object[QStringLiteral("id")].toString();
@ -872,9 +1003,38 @@ QStandardItemModel* GameList::GetModel() const {
return item_model;
}
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs)
{
tree_view->setEnabled(false);
void GameList::UpdateIconSize() {
// Update sizes and stuff for the list view
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
// the scaling on the card is kinda abysmal.
// TODO(crueter): refactor
int heightMargin = 0;
int widthMargin = 80;
switch (icon_size) {
case 128:
heightMargin = 70;
break;
case 0:
widthMargin = 120;
heightMargin = 120;
break;
case 64:
heightMargin = 80;
break;
case 32:
case 256:
heightMargin = 81;
break;
}
// TODO(crueter): Auto size
list_view->setGridSize(QSize(icon_size + widthMargin, icon_size + heightMargin));
m_gameCard->setSize(list_view->gridSize());
}
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
m_currentView->setEnabled(false);
// Update the columns in case UISettings has changed
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
@ -883,6 +1043,8 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs)
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
UpdateIconSize();
// Cancel any existing worker.
current_worker.reset();
@ -890,12 +1052,8 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs)
item_model->removeRows(0, item_model->rowCount());
search_field->clear();
current_worker = std::make_unique<GameListWorker>(vfs,
provider,
game_dirs,
compatibility_list,
play_time_manager,
system);
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
play_time_manager, system);
// Get events from the worker as data becomes available
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
@ -1048,9 +1206,8 @@ void GameList::SetupScrollAnimation() {
// animation handles moving the bar instead of Qt's built in crap
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->setDuration(200);
connect(anim, &QVariantAnimation::valueChanged, this, [bar](const QVariant& value) {
bar->setValue(value.toInt());
});
connect(anim, &QVariantAnimation::valueChanged, this,
[bar](const QVariant& value) { bar->setValue(value.toInt()); });
};
vertical_scroll = new QVariantAnimation(this);
@ -1058,10 +1215,13 @@ void GameList::SetupScrollAnimation() {
setup(vertical_scroll, tree_view->verticalScrollBar());
setup(horizontal_scroll, tree_view->horizontalScrollBar());
setup(vertical_scroll, list_view->verticalScrollBar());
setup(horizontal_scroll, list_view->horizontalScrollBar());
}
bool GameList::eventFilter(QObject* obj, QEvent* event) {
if (obj == tree_view->viewport() && event->type() == QEvent::Wheel) {
if (obj == m_currentView->viewport() && event->type() == QEvent::Wheel) {
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
bool horizontal = wheelEvent->modifiers() & Qt::ShiftModifier;
@ -1078,26 +1238,28 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
// TODO(crueter): dedup this
if (deltaY != 0) {
if (vertical_scroll->state() == QAbstractAnimation::Stopped)
vertical_scroll_target = tree_view->verticalScrollBar()->value();
vertical_scroll_target = m_currentView->verticalScrollBar()->value();
vertical_scroll_target -= deltaY;
vertical_scroll_target = qBound(0, vertical_scroll_target, tree_view->verticalScrollBar()->maximum());
vertical_scroll_target =
qBound(0, vertical_scroll_target, m_currentView->verticalScrollBar()->maximum());
vertical_scroll->stop();
vertical_scroll->setStartValue(tree_view->verticalScrollBar()->value());
vertical_scroll->setStartValue(m_currentView->verticalScrollBar()->value());
vertical_scroll->setEndValue(vertical_scroll_target);
vertical_scroll->start();
}
if (deltaX != 0) {
if (horizontal_scroll->state() == QAbstractAnimation::Stopped)
horizontal_scroll_target = tree_view->horizontalScrollBar()->value();
horizontal_scroll_target = m_currentView->horizontalScrollBar()->value();
horizontal_scroll_target -= deltaX;
horizontal_scroll_target = qBound(0, horizontal_scroll_target, tree_view->horizontalScrollBar()->maximum());
horizontal_scroll_target =
qBound(0, horizontal_scroll_target, m_currentView->horizontalScrollBar()->maximum());
horizontal_scroll->stop();
horizontal_scroll->setStartValue(tree_view->horizontalScrollBar()->value());
horizontal_scroll->setStartValue(m_currentView->horizontalScrollBar()->value());
horizontal_scroll->setEndValue(horizontal_scroll_target);
horizontal_scroll->start();
}

17
src/yuzu/game_list.h → src/yuzu/game/game_list.h

@ -17,6 +17,7 @@
#include <QVBoxLayout>
#include <QVector>
#include <QWidget>
#include <qabstractitemview.h>
#include "common/common_types.h"
#include "core/core.h"
@ -26,6 +27,10 @@
#include "frontend_common/play_time_manager.h"
class QVariantAnimation;
class QListView;
class GameCard;
namespace Core {
class System;
}
@ -92,6 +97,9 @@ public:
static const QStringList supported_file_extensions;
bool IsTreeMode();
void ResetViewMode();
public slots:
void RefreshGameDirectory();
void RefreshExternalContent();
@ -129,6 +137,8 @@ private slots:
void OnFilterCloseClicked();
void OnUpdateThemedIcons();
void UpdateIconSize();
private:
friend class GameListWorker;
void WorkerEvent();
@ -158,7 +168,11 @@ private:
GameListSearchField* search_field;
MainWindow* main_window = nullptr;
QVBoxLayout* layout = nullptr;
QTreeView* tree_view = nullptr;
QListView *list_view = nullptr;
GameCard *m_gameCard = nullptr;
QStandardItemModel* item_model = nullptr;
std::unique_ptr<GameListWorker> current_worker;
QFileSystemWatcher* watcher = nullptr;
@ -178,6 +192,9 @@ private:
const PlayTime::PlayTimeManager& play_time_manager;
Core::System& system;
bool m_isTreeMode = true;
QAbstractItemView *m_currentView = tree_view;
};
class GameListPlaceholder : public QWidget {

45
src/yuzu/game_list_p.h → src/yuzu/game/game_list_p.h

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
@ -75,13 +75,17 @@ public:
GameListItemPath() = default;
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
const QString& game_name, const QString& game_type, u64 program_id) {
const QString& game_name, const QString& game_type, u64 program_id,
u64 play_time) {
setData(type(), TypeRole);
setData(game_path, FullPathRole);
setData(game_name, TitleRole);
setData(qulonglong(program_id), ProgramIdRole);
setData(game_type, FileTypeRole);
setData(QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(play_time)),
Qt::ToolTipRole);
const u32 size = UISettings::values.game_icon_size.GetValue();
QPixmap picture;
@ -111,24 +115,35 @@ public:
}};
const auto& row1 = row_data.at(UISettings::values.row_1_text_id.GetValue());
const int row2_id = UISettings::values.row_2_text_id.GetValue();
// don't show row 2 on grid view
switch (UISettings::values.game_list_mode.GetValue()) {
if (role == SortRole) {
return row1.toLower();
}
case Settings::GameListMode::TreeView: {
const int row2_id = UISettings::values.row_2_text_id.GetValue();
// None
if (row2_id == 4) {
return row1;
}
if (role == SortRole) {
return row1.toLower();
}
const auto& row2 = row_data.at(row2_id);
// None
if (row2_id == 4) {
return row1;
}
if (row1 == row2) {
const auto& row2 = row_data.at(row2_id);
if (row1 == row2) {
return row1;
}
return QStringLiteral("%1\n %2").arg(row1, row2);
}
case Settings::GameListMode::GridView:
return row1;
default:
break;
}
return QStringLiteral("%1\n %2").arg(row1, row2);
}
return GameListItem::data(role);
@ -241,7 +256,9 @@ public:
void setData(const QVariant& value, int role) override {
qulonglong time_seconds = value.toULongLong();
GameListItem::setData(QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)), Qt::DisplayRole);
GameListItem::setData(
QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)),
Qt::DisplayRole);
GameListItem::setData(value, PlayTimeRole);
}

13
src/yuzu/game_list_worker.cpp → src/yuzu/game/game_list_worker.cpp

@ -27,9 +27,9 @@
#include "core/file_sys/submission_package.h"
#include "core/loader/loader.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/game_list.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game_list_worker.h"
#include "yuzu/game/game_list.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/game/game_list_worker.h"
#include "qt_common/config/uisettings.h"
namespace {
@ -214,11 +214,14 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path,
QString patch_versions = GetGameListCachedObject(fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
});
u64 play_time = play_time_manager.GetPlayTime(program_id);
return QList<QStandardItem*>{
new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name), file_type_string, program_id),
new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name),
file_type_string, program_id, play_time),
new GameListItem(file_type_string),
new GameListItemSize(size),
new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
new GameListItemPlayTime(play_time),
new GameListItem(patch_versions),
new GameListItemCompat(compatibility),
};

2
src/yuzu/game_list_worker.h → src/yuzu/game/game_list_worker.h

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project

24
src/yuzu/main.ui

@ -105,6 +105,13 @@
<string>&amp;Debugging</string>
</property>
</widget>
<widget class="QMenu" name="menu_Game_List_Mode">
<property name="title">
<string>&amp;Game List Mode</string>
</property>
<addaction name="action_Tree_View"/>
<addaction name="action_Grid_View"/>
</widget>
<action name="action_Reset_Window_Size_720">
<property name="text">
<string>Reset Window Size to &amp;720p</string>
@ -137,6 +144,7 @@
<addaction name="separator"/>
<addaction name="menu_Reset_Window_Size"/>
<addaction name="menu_View_Debugging"/>
<addaction name="menu_Game_List_Mode"/>
</widget>
<widget class="QMenu" name="menu_Multiplayer">
<property name="enabled">
@ -562,6 +570,22 @@
<string>&amp;Data Manager</string>
</property>
</action>
<action name="action_Tree_View">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Tree View</string>
</property>
</action>
<action name="action_Grid_View">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Grid View</string>
</property>
</action>
</widget>
<resources>
<include location="yuzu.qrc"/>

25
src/yuzu/main_window.cpp

@ -3,6 +3,7 @@
// Qt on macOS doesn't define VMA shit
#include <boost/algorithm/string/split.hpp>
#include "common/settings_enums.h"
#include "frontend_common/settings_generator.h"
#include "qt_common/qt_string_lookup.h"
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
@ -25,7 +26,7 @@
#include "install_dialog.h"
#include "bootmanager.h"
#include "game_list.h"
#include "yuzu/game/game_list.h"
#include "loading_screen.h"
#include "ryujinx_dialog.h"
#include "set_play_time_dialog.h"
@ -550,6 +551,9 @@ MainWindow::MainWindow(bool has_broken_vulkan)
game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.game_dirs);
// Set up game list mode checkboxes.
SetGameListMode(UISettings::values.game_list_mode.GetValue());
// make sure menubar has the arrow cursor instead of inheriting from this
ui->menubar->setCursor(QCursor());
statusBar()->setCursor(QCursor());
@ -1600,6 +1604,9 @@ void MainWindow::ConnectMenuEvents() {
ui->action_Reset_Window_Size_900,
ui->action_Reset_Window_Size_1080});
connect_menu(ui->action_Grid_View, &MainWindow::SetGridView);
connect_menu(ui->action_Tree_View, &MainWindow::SetTreeView);
// Multiplayer
connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnViewLobby);
@ -3373,6 +3380,22 @@ void MainWindow::ResetWindowSize1080() {
ResetWindowSize(Layout::ScreenDocked::Width, Layout::ScreenDocked::Height);
}
void MainWindow::SetGameListMode(Settings::GameListMode mode) {
ui->action_Grid_View->setChecked(mode == Settings::GameListMode::GridView);
ui->action_Tree_View->setChecked(mode == Settings::GameListMode::TreeView);
UISettings::values.game_list_mode = mode;
game_list->ResetViewMode();
}
void MainWindow::SetGridView() {
SetGameListMode(Settings::GameListMode::GridView);
}
void MainWindow::SetTreeView() {
SetGameListMode(Settings::GameListMode::TreeView);
}
void MainWindow::OnConfigure() {
const auto old_theme = UISettings::values.theme;
const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();

6
src/yuzu/main_window.h

@ -17,6 +17,7 @@
#include <QTranslator>
#include "common/common_types.h"
#include "common/settings_enums.h"
#include "frontend_common/content_manager.h"
#include "frontend_common/update_checker.h"
#include "input_common/drivers/tas_input.h"
@ -401,6 +402,11 @@ private slots:
void ResetWindowSize720();
void ResetWindowSize900();
void ResetWindowSize1080();
void SetGameListMode(Settings::GameListMode mode);
void SetGridView();
void SetTreeView();
void LaunchFirmwareApplet(u64 program_id, std::optional<Service::NFP::CabinetMode> mode);
void OnCreateHomeMenuDesktopShortcut();
void OnCreateHomeMenuApplicationMenuShortcut();

4
src/yuzu/multiplayer/chat_room.cpp

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -20,7 +20,7 @@
#include "common/logging/log.h"
#include "network/announce_multiplayer_session.h"
#include "ui_chat_room.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/multiplayer/chat_room.h"
#include "yuzu/multiplayer/message.h"
#ifdef ENABLE_WEB_SERVICE

4
src/yuzu/multiplayer/client_room.cpp

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -14,7 +14,7 @@
#include "common/logging/log.h"
#include "network/announce_multiplayer_session.h"
#include "ui_client_room.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/multiplayer/client_room.h"
#include "yuzu/multiplayer/message.h"
#include "yuzu/multiplayer/moderation_dialog.h"

4
src/yuzu/multiplayer/host_room.cpp

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
@ -19,7 +19,7 @@
#include "core/internal_network/network_interface.h"
#include "network/announce_multiplayer_session.h"
#include "ui_host_room.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/main_window.h"
#include "yuzu/multiplayer/host_room.h"
#include "yuzu/multiplayer/message.h"

4
src/yuzu/multiplayer/lobby.cpp

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
@ -14,7 +14,7 @@
#include "core/internal_network/network_interface.h"
#include "network/network.h"
#include "ui_lobby.h"
#include "yuzu/game_list_p.h"
#include "yuzu/game/game_list_p.h"
#include "yuzu/main_window.h"
#include "yuzu/multiplayer/client_room.h"
#include "yuzu/multiplayer/lobby.h"

4
src/yuzu/multiplayer/state.cpp

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -11,7 +11,7 @@
#include "common/announce_multiplayer_room.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "yuzu/game_list.h"
#include "yuzu/game/game_list.h"
#include "yuzu/multiplayer/client_room.h"
#include "yuzu/multiplayer/direct_connect.h"
#include "yuzu/multiplayer/host_room.h"

Loading…
Cancel
Save