From b9e052b3a7b9d3f265c16e9c5dacb68473b2bd4c Mon Sep 17 00:00:00 2001 From: crueter Date: Fri, 6 Feb 2026 19:51:01 +0100 Subject: [PATCH] [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 Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3479 --- src/common/settings_enums.h | 1 + src/qt_common/config/shared_translation.cpp | 6 + src/qt_common/config/uisettings.h | 4 +- src/yuzu/CMakeLists.txt | 15 +- src/yuzu/game/game_card.cpp | 104 +++++++ src/yuzu/game/game_card.h | 27 ++ src/yuzu/{ => game}/game_list.cpp | 326 +++++++++++++++----- src/yuzu/{ => game}/game_list.h | 17 + src/yuzu/{ => game}/game_list_p.h | 45 ++- src/yuzu/{ => game}/game_list_worker.cpp | 13 +- src/yuzu/{ => game}/game_list_worker.h | 2 +- src/yuzu/main.ui | 24 ++ src/yuzu/main_window.cpp | 25 +- src/yuzu/main_window.h | 6 + src/yuzu/multiplayer/chat_room.cpp | 4 +- src/yuzu/multiplayer/client_room.cpp | 4 +- src/yuzu/multiplayer/host_room.cpp | 4 +- src/yuzu/multiplayer/lobby.cpp | 4 +- src/yuzu/multiplayer/state.cpp | 4 +- 19 files changed, 515 insertions(+), 120 deletions(-) create mode 100644 src/yuzu/game/game_card.cpp create mode 100644 src/yuzu/game/game_card.h rename src/yuzu/{ => game}/game_list.cpp (82%) rename src/yuzu/{ => game}/game_list.h (95%) rename src/yuzu/{ => game}/game_list_p.h (92%) rename src/yuzu/{ => game}/game_list_worker.cpp (98%) rename src/yuzu/{ => game}/game_list_worker.h (97%) diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 30d075565b..33c553dc3c 100644 --- a/src/common/settings_enums.h +++ b/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 inline std::string_view CanonicalizeEnum(Type id) { diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 0d15f9065c..5d4185b47d 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -766,6 +766,12 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) PAIR(ExtendedDynamicState, EDS3, tr("ExtendedDynamicState 3")), }}); + translations->insert({Settings::EnumMetadata::Index(), + { + PAIR(GameListMode, TreeView, tr("Tree View")), + PAIR(GameListMode, GridView, tr("Grid View")), + }}); + #undef PAIR #undef CTX_PAIR diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index 679d00782d..89e3833508 100644 --- a/src/qt_common/config/uisettings.h +++ b/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 folder_icon_size{linkage, 48, "folder_icon_size", Category::UiGameList}; Setting row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList}; Setting row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList}; + Setting game_list_mode{linkage, Settings::GameListMode::TreeView, "game_list_mode", Category::UiGameList}; + std::atomic_bool is_game_list_reload_pending{false}; Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList}; Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList}; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index ba2b5b3927..dad32f2316 100644 --- a/src/yuzu/CMakeLists.txt +++ b/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 diff --git a/src/yuzu/game/game_card.cpp b/src/yuzu/game/game_card.cpp new file mode 100644 index 0000000000..d172d3e535 --- /dev/null +++ b/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 +#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(); + + 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; +} diff --git a/src/yuzu/game/game_card.h b/src/yuzu/game/game_card.h new file mode 100644 index 0000000000..3c695c9047 --- /dev/null +++ b/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 + +/** + * 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; +}; diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game/game_list.cpp similarity index 82% rename from src/yuzu/game_list.cpp rename to src/yuzu/game/game_list.cpp index d206ab096b..515fed1a8d 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game/game_list.cpp @@ -9,29 +9,30 @@ #include #include #include +#include #include -#include #include +#include #include #include #include #include -#include -#include -#include -#include +#include +#include +#include #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& 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& 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& 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& 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& game_dirs) item_model->removeRows(0, item_model->rowCount()); search_field->clear(); - current_worker = std::make_unique(vfs, - provider, - game_dirs, - compatibility_list, - play_time_manager, - system); + current_worker = std::make_unique(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(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(); } diff --git a/src/yuzu/game_list.h b/src/yuzu/game/game_list.h similarity index 95% rename from src/yuzu/game_list.h rename to src/yuzu/game/game_list.h index 9b00e270cd..7de622b714 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game/game_list.h @@ -17,6 +17,7 @@ #include #include #include +#include #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 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 { diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game/game_list_p.h similarity index 92% rename from src/yuzu/game_list_p.h rename to src/yuzu/game/game_list_p.h index ea11d34865..95f5f7eb78 100644 --- a/src/yuzu/game_list_p.h +++ b/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& 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); } diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game/game_list_worker.cpp similarity index 98% rename from src/yuzu/game_list_worker.cpp rename to src/yuzu/game/game_list_worker.cpp index 131d6e6db4..d9c91334e1 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/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 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{ - 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), }; diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game/game_list_worker.h similarity index 97% rename from src/yuzu/game_list_worker.h rename to src/yuzu/game/game_list_worker.h index 1bbb024df3..76153f7917 100644 --- a/src/yuzu/game_list_worker.h +++ b/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 diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 2f968f2b01..54fc778f80 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -105,6 +105,13 @@ &Debugging + + + &Game List Mode + + + + Reset Window Size to &720p @@ -137,6 +144,7 @@ + @@ -562,6 +570,22 @@ &Data Manager + + + true + + + &Tree View + + + + + true + + + &Grid View + + diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index e44696b6a3..def24ef5d3 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -3,6 +3,7 @@ // Qt on macOS doesn't define VMA shit #include +#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(); diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 41c2f4fea0..3261ccc9a1 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -17,6 +17,7 @@ #include #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 mode); void OnCreateHomeMenuDesktopShortcut(); void OnCreateHomeMenuApplicationMenuShortcut(); diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp index 53beda0f8e..a4d343f3a3 100644 --- a/src/yuzu/multiplayer/chat_room.cpp +++ b/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 diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp index 4e995c044f..2d6bcd3bad 100644 --- a/src/yuzu/multiplayer/client_room.cpp +++ b/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" diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 8b811d8878..3e5e42e442 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/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" diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index fab9a56b2c..f28374f75f 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/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" diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index 7549194848..c344bcb8a3 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/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"