From 102d2545303daa926f39e5cf6518c6ce99c7918d Mon Sep 17 00:00:00 2001 From: crueter Date: Fri, 19 Jun 2026 22:55:29 +0200 Subject: [PATCH] [desktop] Clean up game list code, fix external watcher crash, and fix macOS flickering (#4106) - Remove unnecessary icon update code (the UI reloads this stuff anyways); test on Windows please - Cleaned up a bunch of duplicated/unused code within the game list - Fix the game list constantly reloading on macOS * When you reconstruct the entire directory list on the watcher the directoryChanged signal fires on macOS--seems like a behavioral change that occurred somewhere in the 6.8 release cycle--and it would enter an infinite loop very quickly * To fix this, only the differences between the current and old watch list are accounted for on both ends. * Since this bug is now fixed, macOS uses Qt 6.11.1 now. Should theoretically improve our situation. - Fix the external content watcher crashing; the worker would attempt to read files that didn't exist without any bounds since its cache was still pointing to that file. This supersedes and replaces #4099. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4106 Reviewed-by: Lizzie Reviewed-by: MaranBr --- CMakeLists.txt | 7 +- src/qt_common/game_list/game_list_p.h | 52 ++++------- src/qt_common/game_list/model.cpp | 81 +++-------------- src/qt_common/game_list/model.h | 9 +- src/qt_common/game_list/worker.cpp | 81 ++++++++--------- src/yuzu/CMakeLists.txt | 3 +- src/yuzu/game/common.h | 36 ++++++++ src/yuzu/game/game_grid.cpp | 20 +--- src/yuzu/game/game_list.cpp | 126 +++++++++++--------------- src/yuzu/game/game_list.h | 6 +- src/yuzu/game/game_tree.cpp | 26 +----- src/yuzu/main_window.cpp | 11 ++- 12 files changed, 181 insertions(+), 277 deletions(-) create mode 100644 src/yuzu/game/common.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f4667f151..87122e07b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -589,12 +589,7 @@ endif() # Qt stuff if (ENABLE_QT) if (YUZU_USE_BUNDLED_QT) - # Qt 6.8+ is broken on macOS (??) - if (APPLE) - AddQt(Eden-CI/Qt 6.7.3) - else() - AddQt(Eden-CI/Qt 6.11.1) - endif() + AddQt(Eden-CI/Qt 6.11.1) else() message(STATUS "Using system Qt") if (NOT Qt6_DIR) diff --git a/src/qt_common/game_list/game_list_p.h b/src/qt_common/game_list/game_list_p.h index 3994887755..a5297d0db3 100644 --- a/src/qt_common/game_list/game_list_p.h +++ b/src/qt_common/game_list/game_list_p.h @@ -48,6 +48,14 @@ static QPixmap GetDefaultIcon(u32 size) { return icon; } +static QPixmap ThemeIcon(const char* name) { + const int size = UISettings::values.folder_icon_size.GetValue(); + + return QIcon::fromTheme(QLatin1String(name)) + .pixmap(size, size) + .scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +} + class GameListItem : public QStandardItem { public: @@ -296,45 +304,33 @@ public: UISettings::GameDir* game_dir = &directory; setData(QVariant(UISettings::values.game_dirs.indexOf(directory)), GameDirRole); - const int icon_size = UISettings::values.folder_icon_size.GetValue(); + const char* icon_name = nullptr; + switch (dir_type) { case GameListItemType::SdmcDir: - setData( - QIcon::fromTheme(QStringLiteral("sd_card")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + icon_name = "sd_card"; setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole); break; case GameListItemType::UserNandDir: - setData( - QIcon::fromTheme(QStringLiteral("chip")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + icon_name = "chip"; setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole); break; case GameListItemType::SysNandDir: - setData( - QIcon::fromTheme(QStringLiteral("chip")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + icon_name = "chip"; setData(QObject::tr("System Titles"), Qt::DisplayRole); break; case GameListItemType::CustomDir: { const QString path = QString::fromStdString(game_dir->path); - const QString icon_name = - QFileInfo::exists(path) ? QStringLiteral("folder") : QStringLiteral("bad_folder"); - setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( - icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + icon_name = QFileInfo::exists(path) ? "folder" : "bad_folder"; setData(path, Qt::DisplayRole); break; } default: break; } + + if (icon_name != nullptr) + setData(ThemeIcon(icon_name), Qt::DecorationRole); } int type() const override { @@ -357,12 +353,7 @@ public: explicit GameListAddDir() { setData(type(), TypeRole); - const int icon_size = UISettings::values.folder_icon_size.GetValue(); - - setData(QIcon::fromTheme(QStringLiteral("list-add")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + setData(ThemeIcon("list-add"), Qt::DecorationRole); setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole); } @@ -380,12 +371,7 @@ public: explicit GameListFavorites() { setData(type(), TypeRole); - const int icon_size = UISettings::values.folder_icon_size.GetValue(); - - setData(QIcon::fromTheme(QStringLiteral("star")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); + setData(ThemeIcon("star"), Qt::DecorationRole); setData(QObject::tr("Favorites"), Qt::DisplayRole); } diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp index 91ec80c525..5c1042b002 100644 --- a/src/qt_common/game_list/model.cpp +++ b/src/qt_common/game_list/model.cpp @@ -19,8 +19,8 @@ #include "qt_common/util/game.h" #include "qt_common/game_list/game_list_p.h" -#include "qt_common/game_list/worker.h" #include "qt_common/game_list/model.h" +#include "qt_common/game_list/worker.h" GameListModel::GameListModel(std::shared_ptr vfs_, FileSys::ManualContentProvider* provider_, @@ -36,6 +36,8 @@ GameListModel::GameListModel(std::shared_ptr vfs_, connect(external_watcher, &QFileSystemWatcher::directoryChanged, this, &GameListModel::RefreshExternalContent); + ResetExternalWatcher(); + insertColumns(0, COLUMN_COUNT); RetranslateUI(); @@ -45,6 +47,8 @@ GameListModel::GameListModel(std::shared_ptr vfs_, GameListModel::~GameListModel() = default; void GameListModel::PopulateAsync(QVector& game_dirs) { + emit PopulatingStarted(); + current_worker.reset(); removeRows(0, rowCount()); @@ -57,12 +61,6 @@ void GameListModel::PopulateAsync(QVector& game_dirs) { QThreadPool::globalInstance()->start(current_worker.get()); } -void GameListModel::StopWorker() { - // ~GameListWorker sets stop_requested and blocks until run() finishes, so this returns only - // once the worker is no longer touching the content providers. - current_worker.reset(); -} - void GameListModel::WorkerEvent() { current_worker->ProcessEvents(this); } @@ -203,24 +201,25 @@ void GameListModel::LoadCompatibilityList() { } } +void GameListModel::Repopulate() { + current_worker.reset(); + QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); + PopulateAsync(UISettings::values.game_dirs); +} + void GameListModel::RefreshGameDirectory() { ResetExternalWatcher(); - if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) { LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); - StopWorker(); - QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); - PopulateAsync(UISettings::values.game_dirs); + Repopulate(); } } void GameListModel::RefreshExternalContent() { if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) { LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache."); - StopWorker(); QtCommon::Game::ResetMetadata(false); - QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); - PopulateAsync(UISettings::values.game_dirs); + Repopulate(); } } @@ -235,60 +234,6 @@ void GameListModel::ResetExternalWatcher() { } } -void GameListModel::OnUpdateThemedIcons() { - for (int i = 0; i < invisibleRootItem()->rowCount(); i++) { - QStandardItem* child = invisibleRootItem()->child(i); - - const int icon_size = UISettings::values.folder_icon_size.GetValue(); - - switch (child->data(GameListItem::TypeRole).value()) { - case GameListItemType::SdmcDir: - child->setData( - QIcon::fromTheme(QStringLiteral("sd_card")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); - break; - case GameListItemType::UserNandDir: - case GameListItemType::SysNandDir: - child->setData( - QIcon::fromTheme(QStringLiteral("chip")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); - break; - case GameListItemType::CustomDir: { - const UISettings::GameDir& game_dir = - UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()]; - const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path)) - ? QStringLiteral("folder") - : QStringLiteral("bad_folder"); - child->setData( - QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( - icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); - break; - } - case GameListItemType::AddDir: - child->setData( - QIcon::fromTheme(QStringLiteral("list-add")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); - break; - case GameListItemType::Favorites: - child->setData( - QIcon::fromTheme(QStringLiteral("star")) - .pixmap(icon_size) - .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), - Qt::DecorationRole); - break; - default: - break; - } - } -} - void GameListModel::RetranslateUI() { setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, tr("Compatibility")); diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h index ce1fcc87b7..c8661e2fd1 100644 --- a/src/qt_common/game_list/model.h +++ b/src/qt_common/game_list/model.h @@ -3,11 +3,11 @@ #pragma once +#include #include #include #include #include -#include #include "common/common_types.h" #include "frontend_common/play_time_manager.h" @@ -52,10 +52,6 @@ public: void DonePopulating(const QStringList& watch_list); void PopulateAsync(QVector& game_dirs); - // Stops and joins the running populate worker, if any. Must be called before rebuilding the - // content providers (CreateFactories), otherwise the worker keeps scanning a cache that is - // being torn down underneath it. - void StopWorker(); void WorkerEvent(); bool IsEmpty() const; @@ -68,7 +64,6 @@ public: void LoadCompatibilityList(); - void OnUpdateThemedIcons(); void RetranslateUI(); QFileSystemWatcher* GetWatcher() const; @@ -80,6 +75,7 @@ public: signals: void ShowList(bool show); void PopulatingCompleted(const QStringList& watch_list); + void PopulatingStarted(); void SaveConfig(); private: @@ -87,6 +83,7 @@ private: void AddFavorite(u64 program_id); void RemoveFavorite(u64 program_id); + void Repopulate(); bool m_flat = false; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index f1499265c6..5cd68996c5 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -32,11 +32,11 @@ #include "qt_common/config/uisettings.h" #include "qt_common/qt_common.h" -#include "yuzu/compatibility_list.h" #include "qt_common/game_list/game_list_p.h" +#include "yuzu/compatibility_list.h" -#include "qt_common/game_list/worker.h" #include "qt_common/game_list/model.h" +#include "qt_common/game_list/worker.h" namespace { @@ -391,6 +391,25 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa std::vector program_ids; loader->ReadProgramIds(program_ids); + const auto addEntry = [this, physical_name, + parent_dir](std::unique_ptr& app_loader, + const u64 id) { + std::vector icon; + [[maybe_unused]] const auto res1 = app_loader->ReadIcon(icon); + + std::string name = " "; + [[maybe_unused]] const auto res3 = app_loader->ReadTitle(name); + + const FileSys::PatchManager patch{id, system.GetFileSystemController(), + system.GetContentProvider()}; + + auto entry = MakeGameListEntry( + physical_name, name, Common::FS::GetSize(physical_name), icon, *app_loader, + id, compatibility_list, play_time_manager, patch); + + RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); + }; + if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { for (const auto id : program_ids) { @@ -404,38 +423,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa continue; } - std::vector icon; - [[maybe_unused]] const auto res1 = loader->ReadIcon(icon); - - std::string name = " "; - [[maybe_unused]] const auto res3 = loader->ReadTitle(name); - - const FileSys::PatchManager patch{id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *loader, - id, compatibility_list, play_time_manager, patch); - - RecordEvent( - [=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); + addEntry(loader, id); } } else { - std::vector icon; - [[maybe_unused]] const auto res1 = loader->ReadIcon(icon); - - std::string name = " "; - [[maybe_unused]] const auto res3 = loader->ReadTitle(name); - - const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *loader, - program_id, compatibility_list, play_time_manager, patch); - - RecordEvent( - [=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); + addEntry(loader, program_id); } } } else if (is_dir) { @@ -466,29 +457,33 @@ void GameListWorker::run() { break; } + GameListDir* game_list_dir; + bool scan = false; + if (game_dir.path == std::string("SDMC")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir); + game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); } else if (game_dir.path == std::string("UserNAND")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir); + game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); } else if (game_dir.path == std::string("SysNAND")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir); + game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); } else { const QString qpath = QString::fromStdString(game_dir.path); if (QDir(qpath).exists()) { watch_list.append(qpath); } - auto* const game_list_dir = new GameListDir(game_dir); - DirEntryReady(game_list_dir); + + game_list_dir = new GameListDir(game_dir); + scan = true; + } + + DirEntryReady(game_list_dir); + if (scan) { ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan, game_list_dir); ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan, game_list_dir); + } else { + AddTitlesToGameList(game_list_dir); } } diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index a570f5dd41..1e8030947c 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -239,7 +239,8 @@ add_executable(yuzu configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui - updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui) + updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui + game/common.h) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") diff --git a/src/yuzu/game/common.h b/src/yuzu/game/common.h new file mode 100644 index 0000000000..03505d6313 --- /dev/null +++ b/src/yuzu/game/common.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "qt_common/game_list/game_list_p.h" + +namespace Yuzu { + +inline bool ContainsAllWords(const QString& haystack, const QString& userinput) { + const QStringList userinput_split = userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts); + return std::all_of(userinput_split.begin(), userinput_split.end(), + [&haystack](const QString& s) { return haystack.contains(s); }); +} + +inline bool FilterMatches(const QString& filter, const QStandardItem* item) { + if (filter.isEmpty()) + return true; + + const auto program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong(); + + const QString file_path = item->data(GameListItemPath::FullPathRole).toString().toLower(); + const QString file_title = item->data(GameListItemPath::TitleRole).toString().toLower(); + const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'}); + + const QString file_name = + file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + file_title; + + return Yuzu::ContainsAllWords(file_name, filter) || + (file_program_id.size() == 16 && file_program_id.contains(filter)); +} + +} // namespace Yuzu diff --git a/src/yuzu/game/game_grid.cpp b/src/yuzu/game/game_grid.cpp index 88e4a205a8..ab8a3d8bfe 100644 --- a/src/yuzu/game/game_grid.cpp +++ b/src/yuzu/game/game_grid.cpp @@ -5,10 +5,10 @@ #include #include "qt_common/config/uisettings.h" +#include "qt_common/game_list/model.h" +#include "yuzu/game/common.h" #include "yuzu/game/game_card.h" #include "yuzu/game/game_grid.h" -#include "qt_common/game_list/game_list_p.h" -#include "qt_common/game_list/model.h" GameGrid::GameGrid(QWidget* parent) : QListView{parent} { m_gameCard = new GameCard(this); @@ -43,26 +43,12 @@ void GameGrid::SetModel(GameListModel* model) { void GameGrid::ApplyFilter(const QString& edit_filter_text, GameListModel* model) { int row_count = model->rowCount(); - auto ContainsAllWords = [](const QString& haystack, const QString& userinput) { - const QStringList userinput_split = - userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts); - return std::all_of(userinput_split.begin(), userinput_split.end(), - [&haystack](const QString& s) { return haystack.contains(s); }); - }; - for (int i = 0; i < row_count; ++i) { QStandardItem* item = model->item(i, 0); if (!item) continue; - 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)) { + if (Yuzu::FilterMatches(edit_filter_text, item)) { setRowHidden(i, false); } else { setRowHidden(i, true); diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp index 3ea84ba0cb..a1304b4343 100644 --- a/src/yuzu/game/game_list.cpp +++ b/src/yuzu/game/game_list.cpp @@ -22,22 +22,21 @@ #include #include "common/common_types.h" -#include "common/logging.h" #include "core/core.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "qt_common/config/uisettings.h" +#include "qt_common/game_list/game_list_p.h" +#include "qt_common/game_list/model.h" #include "qt_common/qt_common.h" #include "qt_common/util/game.h" #include "yuzu/compatibility_list.h" -#include "yuzu/game/game_list.h" -#include "qt_common/game_list/game_list_p.h" #include "yuzu/game/game_grid.h" +#include "yuzu/game/game_list.h" #include "yuzu/game/game_tree.h" -#include "qt_common/game_list/model.h" +#include "yuzu/game/search_field.h" #include "yuzu/main_window.h" #include "yuzu/util/controller_navigation.h" -#include "yuzu/game/search_field.h" GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, @@ -62,8 +61,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid SetupScrollAnimation(); - connect(main_window, &MainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); - connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); @@ -87,6 +84,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid connect(item_model, &GameListModel::ShowList, this, &GameList::ShowList); connect(item_model, &GameListModel::SaveConfig, this, &GameList::SaveConfig); + connect(item_model, &GameListModel::PopulatingStarted, this, &GameList::OnPopulate); connect(tree_view, &GameTree::FilterResultReady, search_field, [this](int visible, int total) { search_field->setFilterResult(visible, total); }); @@ -137,15 +135,17 @@ void GameList::LoadCompatibilityList() { item_model->LoadCompatibilityList(); } -void GameList::PopulateAsync(QVector& game_dirs) { +void GameList::OnPopulate() { m_currentView->setEnabled(false); - tree_view->UpdateColumnVisibility(item_model); - - if (!m_isTreeMode) { + if (m_isTreeMode) { grid_view->UpdateIconSize(); + } else { + tree_view->UpdateColumnVisibility(item_model); } +} +void GameList::PopulateAsync(QVector& game_dirs) { item_model->PopulateAsync(game_dirs); } @@ -196,8 +196,10 @@ void GameList::ResetViewMode() { auto scroller = QScroller::scroller(view); QScrollerProperties props; - props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); - props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, + QScrollerProperties::OvershootAlwaysOff); + props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, + QScrollerProperties::OvershootAlwaysOff); scroller->setScrollerProperties(props); if (m_isTreeMode != newTreeMode) { @@ -223,10 +225,6 @@ void GameList::OnFilterCloseClicked() { main_window->filterBarSetChecked(false); } -void GameList::OnUpdateThemedIcons() { - item_model->OnUpdateThemedIcons(); -} - void GameList::OnPopulatingCompleted(const QStringList& watch_list) { emit ShowList(!item_model->IsEmpty()); @@ -255,50 +253,44 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) { } } - // Clear out the old directories to watch for changes and add the new ones + // Watcher updates auto* watcher = item_model->GetWatcher(); - const QStringList current_watch = watcher->directories(); + auto current_watch_list = watcher->directories(); - constexpr int LIMIT_WATCH_DIRECTORIES = 5000; + constexpr qsizetype LIMIT_WATCH_DIRECTORIES = 5000; constexpr int SLICE_SIZE = 25; - QStringList desired_watch = watch_list; - if (desired_watch.size() > LIMIT_WATCH_DIRECTORIES) { - desired_watch = desired_watch.mid(0, LIMIT_WATCH_DIRECTORIES); - } + QStringList to_remove, to_add; - // Only re-arm the watcher when the set of directories actually changed. Re-adding the same - // paths makes the macOS QFileSystemWatcher re-emit directoryChanged (the FSEvent arrives - // asynchronously, so the blockSignals guard below does not catch it), which retriggers a full - // refresh and loops forever, making the game list visibly flash. Comparing the sets breaks - // that loop: at steady state we leave the watcher untouched and no spurious event is produced. - QStringList sorted_current = current_watch; - QStringList sorted_desired = desired_watch; - sorted_current.sort(); - sorted_desired.sort(); - - if (sorted_current != sorted_desired) { - if (!current_watch.isEmpty()) { - watcher->removePaths(current_watch); - } - -#ifdef __APPLE__ - const bool old_signals_blocked = watcher->blockSignals(true); -#endif - - for (int i = 0; i < desired_watch.size(); i += SLICE_SIZE) { - auto chunk = desired_watch.mid(i, SLICE_SIZE); + const auto slice = [&](const QStringList& list, std::function callback) { + const int len = (std::min)(list.size(), LIMIT_WATCH_DIRECTORIES); + for (int i = 0; i < len; i += SLICE_SIZE) { + auto chunk = list.mid(i, SLICE_SIZE); if (!chunk.isEmpty()) { - watcher->addPaths(chunk); + callback(chunk); } QCoreApplication::processEvents(); } + }; -#ifdef __APPLE__ - watcher->blockSignals(old_signals_blocked); -#endif + // remove any paths not in the new watch list + for (const auto& path : std::as_const(current_watch_list)) { + if (!watch_list.contains(path)) { + to_remove.emplaceBack(path); + } } + slice(to_remove, [watcher](const QStringList& chunk) { watcher->removePaths(chunk); }); + + // add any paths not in the old watch list + for (const auto& path : std::as_const(watch_list)) { + if (!current_watch_list.contains(path)) { + to_add.emplaceBack(path); + } + } + + slice(to_add, [watcher](const QStringList& chunk) { watcher->addPaths(chunk); }); + m_currentView->setEnabled(true); int children_total = 0; @@ -317,23 +309,16 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) { } void GameList::RefreshGameDirectory() { - item_model->ResetExternalWatcher(); - - if (!UISettings::values.game_dirs.empty()) { - LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); - item_model->StopWorker(); - QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); - PopulateAsync(UISettings::values.game_dirs); - } + item_model->RefreshGameDirectory(); } void GameList::RefreshExternalContent() { - if (!UISettings::values.game_dirs.empty()) { - LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache."); - item_model->StopWorker(); - QtCommon::Game::ResetMetadata(false); - QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); - PopulateAsync(UISettings::values.game_dirs); + item_model->RefreshExternalContent(); +} + +void GameList::UpdateIconSizes() { + if (!m_isTreeMode) { + grid_view->UpdateIconSize(); } } @@ -491,9 +476,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri }); connect(start_game, &QAction::triggered, this, [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Normal); }); - connect(start_game_global, &QAction::triggered, this, [this, path]() { - emit BootGame(QString::fromStdString(path), StartGameType::Global); - }); + connect(start_game_global, &QAction::triggered, this, + [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Global); }); connect(open_mod_location, &QAction::triggered, this, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path); }); @@ -632,7 +616,8 @@ void GameList::AddFavoritesPopup(QMenu& context_menu) { connect(clear, &QAction::triggered, this, [this] { UISettings::values.favorited_ids.clear(); - item_model->invisibleRootItem()->child(0)->removeRows(0, item_model->invisibleRootItem()->child(0)->rowCount()); + item_model->invisibleRootItem()->child(0)->removeRows( + 0, item_model->invisibleRootItem()->child(0)->rowCount()); tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); }); } @@ -731,9 +716,6 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) { } GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} { - connect(parent, &MainWindow::UpdateThemedIcons, this, - &GameListPlaceholder::onUpdateThemedIcons); - layout = new QVBoxLayout; image = new QLabel; text = new QLabel; @@ -754,10 +736,6 @@ GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} { GameListPlaceholder::~GameListPlaceholder() = default; -void GameListPlaceholder::onUpdateThemedIcons() { - image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); -} - void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { emit GameListPlaceholder::AddDirectory(); } diff --git a/src/yuzu/game/game_list.h b/src/yuzu/game/game_list.h index de0db789aa..4c4c5150fe 100644 --- a/src/yuzu/game/game_list.h +++ b/src/yuzu/game/game_list.h @@ -89,6 +89,8 @@ public: public slots: void RefreshGameDirectory(); void RefreshExternalContent(); + void UpdateIconSizes(); + void OnPopulate(); signals: void BootGame(const QString& game_path, StartGameType type); @@ -119,7 +121,6 @@ signals: private slots: void OnTextChanged(const QString& new_text); void OnFilterCloseClicked(); - void OnUpdateThemedIcons(); void OnPopulatingCompleted(const QStringList& watch_list); private: @@ -175,9 +176,6 @@ public: signals: void AddDirectory(); -private slots: - void onUpdateThemedIcons(); - protected: void mouseDoubleClickEvent(QMouseEvent* event) override; diff --git a/src/yuzu/game/game_tree.cpp b/src/yuzu/game/game_tree.cpp index f670d03dc6..b197eaad03 100644 --- a/src/yuzu/game/game_tree.cpp +++ b/src/yuzu/game/game_tree.cpp @@ -8,8 +8,9 @@ #include "qt_common/config/uisettings.h" #include "qt_common/game_list/game_list_p.h" -#include "yuzu/game/game_tree.h" #include "qt_common/game_list/model.h" +#include "yuzu/game/common.h" +#include "yuzu/game/game_tree.h" GameTree::GameTree(QWidget* parent) : QTreeView{parent} { setAlternatingRowColors(true); @@ -139,28 +140,7 @@ void GameTree::ApplyFilter(const QString& edit_filter_text, GameListModel* model const QStandardItem* child = folder->child(j, 0); - const auto program_id = child->data(GameListItemPath::ProgramIdRole).toULongLong(); - - const QString file_path = - child->data(GameListItemPath::FullPathRole).toString().toLower(); - const QString file_title = - child->data(GameListItemPath::TitleRole).toString().toLower(); - const QString file_program_id = - QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'}); - - const QString file_name = - file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + - file_title; - - auto ContainsAllWords = [](const QString& haystack, const QString& userinput) { - const QStringList userinput_split = - userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts); - return std::all_of(userinput_split.begin(), userinput_split.end(), - [&haystack](const QString& s) { return haystack.contains(s); }); - }; - - if (ContainsAllWords(file_name, edit_filter_text) || - (file_program_id.size() == 16 && file_program_id.contains(edit_filter_text))) { + if (Yuzu::FilterMatches(edit_filter_text, child)) { setRowHidden(j, folder_index, false); ++result_count; } else { diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 0160b74e24..00b9e65c67 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -579,9 +579,12 @@ MainWindow::MainWindow(bool has_broken_vulkan) } else if (should_launch_qlaunch) { LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::QLaunch), std::nullopt); } else if (should_launch_hlaunch) { - std::filesystem::path const sd_dir = Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir); + std::filesystem::path const sd_dir = + Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir); auto const hbl_path = (sd_dir / "atmosphere" / "hbl.nsp").string(); - BootGame(QString::fromStdString(hbl_path), LibraryAppletParameters(0x010000000000100Dull, Service::AM::AppletId::QLaunch)); + BootGame( + QString::fromStdString(hbl_path), + LibraryAppletParameters(0x010000000000100Dull, Service::AM::AppletId::QLaunch)); } } } @@ -1545,6 +1548,8 @@ void MainWindow::ConnectMenuEvents() { if (checked) { UISettings::values.game_icon_size.SetValue(size); CheckIconSize(); + + game_list->UpdateIconSizes(); game_list->RefreshGameDirectory(); } }); @@ -3363,6 +3368,7 @@ void MainWindow::ToggleShowGameName() { CheckIconSize(); + game_list->UpdateIconSizes(); game_list->RefreshGameDirectory(); } @@ -3868,6 +3874,7 @@ void MainWindow::OnGameListRefresh() { // Resets metadata cache and reloads QtCommon::Game::ResetMetadata(false); game_list->RefreshGameDirectory(); + game_list->RefreshExternalContent(); SetFirmwareVersion(); }