aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcrueter <crueter@eden-emu.dev>2026-06-02 04:08:24 +0200
committercrueter <crueter@eden-emu.dev>2026-06-02 04:08:24 +0200
commitcc8451f764979db23208dd5a55884cd62fd8b673 (patch)
tree5fe5dc8e28322f897492639e7c895d226f9bcdc5 /src
parent27189f39d2bf78c924475ac6ea2a79c3d6ec6d8a (diff)
[desktop] Rework game list to use MVP architecture (#4042)
Closes #3480 moves the game list model/worker/private stuff to qt_common for later use in QML - `qt_common/game_list/model.{cpp,h}` is the model - `yuzu/game/game_{grid,tree}.*` are the views - `yuzu/game/game_list.cpp` is the presenter This was done very lazily in a manner that "works" while largely maintaining existing structure as much as possible. Most of it is copy-paste, with some bonus reworks/cleanups thrown in. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4042 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Diffstat (limited to 'src')
-rw-r--r--src/qt_common/CMakeLists.txt4
-rw-r--r--src/qt_common/game_list/game_list_p.h (renamed from src/yuzu/game/game_list_p.h)53
-rw-r--r--src/qt_common/game_list/model.cpp303
-rw-r--r--src/qt_common/game_list/model.h98
-rw-r--r--src/qt_common/game_list/worker.cpp (renamed from src/yuzu/game/game_list_worker.cpp)23
-rw-r--r--src/qt_common/game_list/worker.h (renamed from src/yuzu/game/game_list_worker.h)12
-rw-r--r--src/qt_common/qt_common.cpp17
-rw-r--r--src/qt_common/qt_common.h14
-rw-r--r--src/yuzu/CMakeLists.txt26
-rw-r--r--src/yuzu/configuration/configure_per_game.cpp3
-rw-r--r--src/yuzu/game/game_card.cpp83
-rw-r--r--src/yuzu/game/game_grid.cpp118
-rw-r--r--src/yuzu/game/game_grid.h26
-rw-r--r--src/yuzu/game/game_list.cpp1069
-rw-r--r--src/yuzu/game/game_list.h69
-rw-r--r--src/yuzu/game/game_tree.cpp173
-rw-r--r--src/yuzu/game/game_tree.h35
-rw-r--r--src/yuzu/game/search_field.cpp124
-rw-r--r--src/yuzu/game/search_field.h52
-rw-r--r--src/yuzu/multiplayer/chat_room.cpp2
-rw-r--r--src/yuzu/multiplayer/client_room.cpp2
-rw-r--r--src/yuzu/multiplayer/host_room.cpp2
-rw-r--r--src/yuzu/multiplayer/lobby.cpp2
-rw-r--r--src/yuzu/util/util.cpp18
-rw-r--r--src/yuzu/util/util.h10
25 files changed, 1300 insertions, 1038 deletions
diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt
index a8c4e51240..8858b819d6 100644
--- a/src/qt_common/CMakeLists.txt
+++ b/src/qt_common/CMakeLists.txt
@@ -31,6 +31,10 @@ add_library(qt_common STATIC
abstract/frontend.h abstract/frontend.cpp
abstract/progress.h abstract/progress.cpp
+ game_list/model.h game_list/model.cpp
+ game_list/worker.h game_list/worker.cpp
+ game_list/game_list_p.h
+
qt_string_lookup.h
qt_compat.h
diff --git a/src/yuzu/game/game_list_p.h b/src/qt_common/game_list/game_list_p.h
index f9d714fb8b..3994887755 100644
--- a/src/yuzu/game/game_list_p.h
+++ b/src/qt_common/game_list/game_list_p.h
@@ -17,14 +17,13 @@
#include <QRegularExpression>
#include <QStandardItem>
#include <QString>
-#include <QWidget>
#include "common/common_types.h"
#include "common/logging.h"
#include "common/string_util.h"
#include "frontend_common/play_time_manager.h"
#include "qt_common/config/uisettings.h"
-#include "yuzu/util/util.h"
+#include "qt_common/qt_common.h"
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
@@ -204,7 +203,7 @@ public:
setData(compatibility, CompatNumberRole);
setText(tr(status.text));
setToolTip(tr(status.tooltip));
- setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
+ setData(QtCommon::CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
}
int type() const override {
@@ -237,7 +236,7 @@ public:
// representations of the data are always accurate and in the correct format.
if (role == SizeRole) {
qulonglong size_bytes = value.toULongLong();
- GameListItem::setData(ReadableByteSize(size_bytes), Qt::DisplayRole);
+ GameListItem::setData(QtCommon::ReadableByteSize(size_bytes), Qt::DisplayRole);
GameListItem::setData(value, SizeRole);
} else {
GameListItem::setData(value, role);
@@ -398,49 +397,3 @@ public:
return false;
}
};
-
-class GameList;
-class QHBoxLayout;
-class QTreeView;
-class QLabel;
-class QLineEdit;
-class QToolButton;
-
-class GameListSearchField : public QWidget {
- Q_OBJECT
-
-public:
- explicit GameListSearchField(GameList* parent = nullptr);
-
- QString filterText() const;
- void setFilterResult(int visible_, int total_);
-
- void clear();
- void setFocus();
-
-private:
- void changeEvent(QEvent*) override;
- void RetranslateUI();
-
- class KeyReleaseEater : public QObject {
- public:
- explicit KeyReleaseEater(GameList* gamelist_, QObject* parent = nullptr);
-
- private:
- GameList* gamelist = nullptr;
- QString edit_filter_text_old;
-
- protected:
- // EventFilter in order to process systemkeys while editing the searchfield
- bool eventFilter(QObject* obj, QEvent* event) override;
- };
- int visible;
- int total;
-
- QHBoxLayout* layout_filter = nullptr;
- QTreeView* tree_view = nullptr;
- QLabel* label_filter = nullptr;
- QLineEdit* edit_filter = nullptr;
- QLabel* label_filter_result = nullptr;
- QToolButton* button_filter_close = nullptr;
-};
diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp
new file mode 100644
index 0000000000..2f8deaa0c5
--- /dev/null
+++ b/src/qt_common/game_list/model.cpp
@@ -0,0 +1,303 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <QDir>
+#include <QIcon>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QThreadPool>
+
+#include "common/logging.h"
+#include "common/settings.h"
+#include "core/core.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "qt_common/config/uisettings.h"
+#include "qt_common/qt_common.h"
+#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"
+
+GameListModel::GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
+ FileSys::ManualContentProvider* provider_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_, QObject* parent)
+ : QStandardItemModel{parent}, vfs{std::move(vfs_)}, provider{provider_},
+ play_time_manager{play_time_manager_}, system{system_} {
+ watcher = new QFileSystemWatcher(this);
+ external_watcher = new QFileSystemWatcher(this);
+
+ connect(watcher, &QFileSystemWatcher::directoryChanged, this,
+ &GameListModel::RefreshGameDirectory);
+ connect(external_watcher, &QFileSystemWatcher::directoryChanged, this,
+ &GameListModel::RefreshExternalContent);
+
+ insertColumns(0, COLUMN_COUNT);
+ RetranslateUI();
+
+ setSortRole(GameListItemPath::SortRole);
+}
+
+GameListModel::~GameListModel() = default;
+
+void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
+ current_worker.reset();
+ removeRows(0, rowCount());
+
+ current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
+ play_time_manager, system);
+
+ connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameListModel::WorkerEvent,
+ Qt::QueuedConnection);
+
+ QThreadPool::globalInstance()->start(current_worker.get());
+}
+
+void GameListModel::WorkerEvent() {
+ current_worker->ProcessEvents(this);
+}
+
+void GameListModel::AddDirEntry(GameListDir* entry_items) {
+ if (m_flat) {
+ return;
+ }
+ invisibleRootItem()->appendRow(entry_items);
+}
+
+void GameListModel::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
+ if (m_flat) {
+ invisibleRootItem()->appendRow(entry_items);
+ } else {
+ parent->appendRow(entry_items);
+ }
+}
+
+void GameListModel::DonePopulating(const QStringList& watch_list) {
+ emit ShowList(!IsEmpty());
+
+ if (!m_flat) {
+ invisibleRootItem()->appendRow(new GameListAddDir());
+ invisibleRootItem()->insertRow(0, new GameListFavorites());
+
+ for (const auto id : std::as_const(UISettings::values.favorited_ids)) {
+ AddFavorite(id);
+ }
+ }
+
+ emit PopulatingCompleted(watch_list);
+}
+
+bool GameListModel::IsEmpty() const {
+ for (int i = 0; i < rowCount(); i++) {
+ const QStandardItem* child = invisibleRootItem()->child(i);
+ const auto type = static_cast<GameListItemType>(child->type());
+
+ if (!child->hasChildren() &&
+ (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
+ type == GameListItemType::SysNandDir)) {
+ invisibleRootItem()->removeRow(child->row());
+ i--;
+ }
+ }
+
+ return !invisibleRootItem()->hasChildren();
+}
+
+void GameListModel::ToggleFavorite(u64 program_id) {
+ if (!UISettings::values.favorited_ids.contains(program_id)) {
+ UISettings::values.favorited_ids.append(program_id);
+ AddFavorite(program_id);
+ } else {
+ UISettings::values.favorited_ids.removeOne(program_id);
+ RemoveFavorite(program_id);
+ }
+ emit SaveConfig();
+}
+
+void GameListModel::AddFavorite(u64 program_id) {
+ auto* favorites_row = item(0);
+
+ for (int i = 1; i < rowCount() - 1; i++) {
+ const auto* folder = item(i);
+ for (int j = 0; j < folder->rowCount(); j++) {
+ if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+ program_id) {
+ QList<QStandardItem*> list;
+ for (int k = 0; k < COLUMN_COUNT; k++) {
+ list.append(folder->child(j, k)->clone());
+ }
+ list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
+ GameListItem::SortRole);
+ list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
+
+ favorites_row->appendRow(list);
+ return;
+ }
+ }
+ }
+}
+
+void GameListModel::RemoveFavorite(u64 program_id) {
+ auto* favorites_row = item(0);
+
+ for (int i = 0; i < favorites_row->rowCount(); i++) {
+ const auto* game = favorites_row->child(i);
+ if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
+ favorites_row->removeRow(i);
+ return;
+ }
+ }
+}
+
+void GameListModel::LoadCompatibilityList() {
+ QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")};
+
+ if (!compat_list.open(QFile::ReadOnly | QFile::Text)) {
+ LOG_ERROR(Frontend, "Unable to open game compatibility list");
+ return;
+ }
+
+ if (compat_list.size() == 0) {
+ LOG_WARNING(Frontend, "Game compatibility list is empty");
+ return;
+ }
+
+ const QByteArray content = compat_list.readAll();
+ if (content.isEmpty()) {
+ LOG_ERROR(Frontend, "Unable to completely read game compatibility list");
+ return;
+ }
+
+ const QJsonDocument json = QJsonDocument::fromJson(content);
+ const QJsonArray arr = json.array();
+
+ for (const QJsonValue& value : arr) {
+ const QJsonObject game = value.toObject();
+ const QString compatibility_key = QStringLiteral("compatibility");
+
+ if (!game.contains(compatibility_key) || !game[compatibility_key].isDouble()) {
+ continue;
+ }
+
+ const int compatibility = game[compatibility_key].toInt();
+ const QString directory = game[QStringLiteral("directory")].toString();
+ const QJsonArray ids = game[QStringLiteral("releases")].toArray();
+
+ for (const QJsonValue& id_ref : ids) {
+ const QJsonObject id_object = id_ref.toObject();
+ const QString id = id_object[QStringLiteral("id")].toString();
+
+ compatibility_list.emplace(id.toUpper().toStdString(),
+ std::make_pair(QString::number(compatibility), directory));
+ }
+ }
+}
+
+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.");
+ QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
+ PopulateAsync(UISettings::values.game_dirs);
+ }
+}
+
+void GameListModel::RefreshExternalContent() {
+ if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
+ LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache.");
+ QtCommon::Game::ResetMetadata(false);
+ QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
+ PopulateAsync(UISettings::values.game_dirs);
+ }
+}
+
+void GameListModel::ResetExternalWatcher() {
+ auto watch_dirs = external_watcher->directories();
+ if (!watch_dirs.isEmpty()) {
+ external_watcher->removePaths(watch_dirs);
+ }
+
+ for (const std::string& dir : Settings::values.external_content_dirs) {
+ external_watcher->addPath(QString::fromStdString(dir));
+ }
+}
+
+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<GameListItemType>()) {
+ 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"));
+ setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
+ setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
+ setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
+ setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
+}
+
+QFileSystemWatcher* GameListModel::GetWatcher() const {
+ return watcher;
+}
+
+const CompatibilityList& GameListModel::GetCompatibilityList() const {
+ return compatibility_list;
+}
+
+void GameListModel::SetFlat(bool flat) {
+ m_flat = flat;
+}
diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h
new file mode 100644
index 0000000000..d0a116beb6
--- /dev/null
+++ b/src/qt_common/game_list/model.h
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QFileSystemWatcher>
+#include <QStandardItemModel>
+#include <QStringList>
+#include <QVector>
+#include <memory>
+
+#include "common/common_types.h"
+#include "frontend_common/play_time_manager.h"
+#include "qt_common/config/uisettings.h"
+#include "yuzu/compatibility_list.h"
+
+namespace Core {
+class System;
+}
+
+class GameListDir;
+class GameListWorker;
+class QStandardItem;
+
+namespace FileSys {
+class ManualContentProvider;
+class VfsFilesystem;
+} // namespace FileSys
+
+class GameListModel : public QStandardItemModel {
+ Q_OBJECT
+
+public:
+ enum Column {
+ COLUMN_NAME,
+ COLUMN_FILE_TYPE,
+ COLUMN_SIZE,
+ COLUMN_PLAY_TIME,
+ COLUMN_ADD_ONS,
+ COLUMN_COMPATIBILITY,
+ COLUMN_COUNT,
+ };
+
+ explicit GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
+ FileSys::ManualContentProvider* provider_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_, QObject* parent = nullptr);
+ ~GameListModel() override;
+
+ void AddDirEntry(GameListDir* entry_items);
+ void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
+ void DonePopulating(const QStringList& watch_list);
+
+ void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
+ void WorkerEvent();
+
+ bool IsEmpty() const;
+
+ void ToggleFavorite(u64 program_id);
+
+ void RefreshGameDirectory();
+ void RefreshExternalContent();
+ void ResetExternalWatcher();
+
+ void LoadCompatibilityList();
+
+ void OnUpdateThemedIcons();
+ void RetranslateUI();
+
+ QFileSystemWatcher* GetWatcher() const;
+
+ const CompatibilityList& GetCompatibilityList() const;
+
+ void SetFlat(bool flat);
+
+signals:
+ void ShowList(bool show);
+ void PopulatingCompleted(const QStringList& watch_list);
+ void SaveConfig();
+
+private:
+ friend class GameListWorker;
+
+ void AddFavorite(u64 program_id);
+ void RemoveFavorite(u64 program_id);
+
+ bool m_flat = false;
+
+ std::shared_ptr<FileSys::VfsFilesystem> vfs;
+ FileSys::ManualContentProvider* provider;
+ CompatibilityList compatibility_list;
+ const PlayTime::PlayTimeManager& play_time_manager;
+ Core::System& system;
+
+ std::unique_ptr<GameListWorker> current_worker;
+ QFileSystemWatcher* watcher = nullptr;
+ QFileSystemWatcher* external_watcher = nullptr;
+};
diff --git a/src/yuzu/game/game_list_worker.cpp b/src/qt_common/game_list/worker.cpp
index df3560a813..f1499265c6 100644
--- a/src/yuzu/game/game_list_worker.cpp
+++ b/src/qt_common/game_list/worker.cpp
@@ -33,9 +33,10 @@
#include "qt_common/qt_common.h"
#include "yuzu/compatibility_list.h"
-#include "yuzu/game/game_list.h"
-#include "yuzu/game/game_list_p.h"
-#include "yuzu/game/game_list_worker.h"
+#include "qt_common/game_list/game_list_p.h"
+
+#include "qt_common/game_list/worker.h"
+#include "qt_common/game_list/model.h"
namespace {
@@ -250,9 +251,9 @@ GameListWorker::~GameListWorker() {
processing_completed.Wait();
}
-void GameListWorker::ProcessEvents(GameList* game_list) {
+void GameListWorker::ProcessEvents(GameListModel* model) {
while (true) {
- std::function<void(GameList*)> func;
+ std::function<void(GameListModel*)> func;
{
// Lock queue to protect concurrent modification.
std::scoped_lock lk(lock);
@@ -268,7 +269,7 @@ void GameListWorker::ProcessEvents(GameList* game_list) {
}
// Run the function.
- func(game_list);
+ func(model);
}
}
@@ -335,7 +336,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
program_id, compatibility_list, play_time_manager, patch);
- RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
+ RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
}
}
@@ -417,7 +418,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
id, compatibility_list, play_time_manager, patch);
RecordEvent(
- [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
+ [=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
}
} else {
std::vector<u8> icon;
@@ -434,7 +435,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
program_id, compatibility_list, play_time_manager, patch);
RecordEvent(
- [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
+ [=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
}
}
} else if (is_dir) {
@@ -457,7 +458,7 @@ void GameListWorker::run() {
provider->ClearAllEntries();
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
- RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
+ RecordEvent([=](GameListModel* model) { model->AddDirEntry(game_list_dir); });
};
for (UISettings::GameDir& game_dir : game_dirs) {
@@ -491,6 +492,6 @@ void GameListWorker::run() {
}
}
- RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); });
+ RecordEvent([this](GameListModel* model) { model->DonePopulating(watch_list); });
processing_completed.Set();
}
diff --git a/src/yuzu/game/game_list_worker.h b/src/qt_common/game_list/worker.h
index bf67585fe6..c11741c5f5 100644
--- a/src/yuzu/game/game_list_worker.h
+++ b/src/qt_common/game_list/worker.h
@@ -26,8 +26,8 @@ namespace Core {
class System;
}
-class GameList;
class GameListDir;
+class GameListModel;
class QStandardItem;
namespace FileSys {
@@ -58,11 +58,11 @@ public:
/**
* Synchronously processes any events queued by the worker.
*
- * AddDirEntry is called on the game list for every discovered directory.
- * AddEntry is called on the game list for every discovered program.
- * DonePopulating is called on the game list when processing completes.
+ * AddDirEntry is called on the model for every discovered directory.
+ * AddEntry is called on the model for every discovered program.
+ * DonePopulating is called on the model when processing completes.
*/
- void ProcessEvents(GameList* game_list);
+ void ProcessEvents(GameListModel* model);
signals:
void DataAvailable();
@@ -92,7 +92,7 @@ private:
std::mutex lock;
std::condition_variable cv;
- std::deque<std::function<void(GameList*)>> queued_events;
+ std::deque<std::function<void(GameListModel*)>> queued_events;
std::atomic_bool stop_requested = false;
Common::Event processing_completed;
diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp
index 98de0a6317..b9b8caf6e8 100644
--- a/src/qt_common/qt_common.cpp
+++ b/src/qt_common/qt_common.cpp
@@ -5,6 +5,7 @@
#include "common/memory_detect.h"
#include "core/hle/service/filesystem/filesystem.h"
+#include "frontend_common/data_manager.h"
#include "hid_core/hid_core.h"
#include "network/network.h"
#include "qt_common.h"
@@ -27,6 +28,7 @@
#include <thread>
#include <JlCompress.h>
+#include <QPainter>
#if !defined(WIN32) && !defined(__APPLE__)
#include <qpa/qplatformnativeinterface.h>
@@ -307,4 +309,19 @@ void SetupHID() {
system->HIDCore().ReloadInputDevices();
}
+QString ReadableByteSize(qulonglong size) {
+ return QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size));
+}
+
+QPixmap CreateCirclePixmapFromColor(const QColor& color) {
+ QPixmap circle_pixmap(16, 16);
+ circle_pixmap.fill(Qt::transparent);
+ QPainter painter(&circle_pixmap);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setPen(color);
+ painter.setBrush(color);
+ painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0);
+ return circle_pixmap;
+}
+
} // namespace QtCommon
diff --git a/src/qt_common/qt_common.h b/src/qt_common/qt_common.h
index dd860ef5f7..2109774506 100644
--- a/src/qt_common/qt_common.h
+++ b/src/qt_common/qt_common.h
@@ -46,6 +46,20 @@ void SetupHID();
const QString tr(const char* str);
const QString tr(const std::string& str);
+// TODO: Find a better place for these
+
+/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
+[[nodiscard]] QString ReadableByteSize(qulonglong size);
+
+/**
+ * Creates a circle pixmap from a specified color
+ * @param color The color the pixmap shall have
+ * @return QPixmap circle pixmap
+ */
+[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
+
+
+
std::filesystem::path GetEdenCommand();
} // namespace QtCommon
#endif
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 0bddcca3e8..a570f5dd41 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -156,13 +156,12 @@ add_executable(yuzu
debugger/controller.cpp
debugger/controller.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
+ game/game_list.cpp game/game_list.h
+ game/game_grid.cpp game/game_grid.h
+
+ game/game_tree.h game/game_tree.cpp
+ game/game_card.h game/game_card.cpp
+ game/search_field.h game/search_field.cpp
hotkeys.cpp
hotkeys.h
@@ -219,18 +218,16 @@ add_executable(yuzu
util/util.h
compatdb.cpp
compatdb.h
- user_data_migration.cpp
- user_data_migration.h
+
+ user_data_migration.h user_data_migration.cpp
yuzu.qrc
yuzu.rc
migration_dialog.h migration_dialog.cpp
- migration_worker.h
- migration_worker.cpp
+ migration_worker.h migration_worker.cpp
+ libqt_common.h libqt_common.cpp
- deps_dialog.cpp
- deps_dialog.h
- deps_dialog.ui
+ deps_dialog.cpp deps_dialog.h deps_dialog.ui
data_dialog.h data_dialog.cpp data_dialog.ui
data_widget.ui
@@ -242,7 +239,6 @@ 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
- libqt_common.h libqt_common.cpp
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui)
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp
index f84448dd29..6f530eb0ab 100644
--- a/src/yuzu/configuration/configure_per_game.cpp
+++ b/src/yuzu/configuration/configure_per_game.cpp
@@ -30,6 +30,7 @@
#include "core/loader/loader.h"
#include "frontend_common/config.h"
#include "qt_common/config/uisettings.h"
+#include "qt_common/qt_common.h"
#include "qt_common/util/vk.h"
#include "ui_configure_per_game.h"
#include "yuzu/configuration/configuration_shared.h"
@@ -205,6 +206,6 @@ void ConfigurePerGame::LoadConfiguration() {
ui->display_format->setText(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())));
- const auto valueText = ReadableByteSize(file->GetSize());
+ const auto valueText = QtCommon::ReadableByteSize(file->GetSize());
ui->display_size->setText(valueText);
}
diff --git a/src/yuzu/game/game_card.cpp b/src/yuzu/game/game_card.cpp
index 03c71449f8..e1cd92652e 100644
--- a/src/yuzu/game/game_card.cpp
+++ b/src/yuzu/game/game_card.cpp
@@ -18,34 +18,27 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
- // Padding, dimensions, alignment...
+ constexpr int cardMargin = 8;
+ constexpr int cardCornerRadius = 10;
+
const int column = index.row() % m_columns;
const int cell_width = option.rect.width();
- const int fixed_card_width = cell_width - m_padding;
- const int margins = 8;
-
- // The gist of it is that this anchors the left and right sides to the edges,
- // while maintaining an even gap between each card.
- // I just smashed random keys into my keyboard until something worked.
- // Don't even bother trying to figure out what the hell this is doing.
- const auto total_row_width = m_columns * cell_width;
- const auto total_gap_space = total_row_width - (margins * 2) - (m_columns * fixed_card_width);
- const auto gap = (m_columns > 1) ? (total_gap_space / (m_columns - 1)) : 0;
+ const int card_width = cell_width - m_padding;
- const auto relative_x = margins + (column * (fixed_card_width + gap));
- const auto x_pos = option.rect.left() - (column * cell_width) + static_cast<int>(relative_x);
+ const int row_width = m_columns * cell_width;
+ const int total_gap = row_width - cardMargin * 2 - m_columns * card_width;
+ const int gap = (m_columns > 1) ? (total_gap / (m_columns - 1)) : 0;
- // also, add some additional padding here to prevent card overlap
- QRect cardRect(x_pos + 4, option.rect.top() + 4, fixed_card_width - 8,
- option.rect.height() - margins);
+ const int card_left = option.rect.left() - column * cell_width + cardMargin + column * (card_width + gap) + 4;
+ const QRect cardRect(card_left, option.rect.top() + 4, card_width - 8,
+ option.rect.height() - cardMargin);
- // 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
+ // highlight blue on select
if (option.state & QStyle::State_Selected) {
backgroundColor = palette.highlight().color();
borderColor = palette.highlight().color().lighter(150);
@@ -54,63 +47,45 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
backgroundColor = backgroundColor.lighter(120);
}
- // bg
painter->setBrush(backgroundColor);
painter->setPen(QPen(borderColor, 1));
- painter->drawRoundedRect(cardRect, 10, 10);
+ painter->drawRoundedRect(cardRect, cardCornerRadius, cardCornerRadius);
- // icon
- int _iconsize = UISettings::values.game_icon_size.GetValue();
- QSize iconSize(_iconsize, _iconsize);
- QPixmap iconPixmap = index.data(Qt::DecorationRole).value<QPixmap>();
+ const u32 icon_size = UISettings::values.game_icon_size.GetValue();
+ QPixmap icon_pixmap = index.data(Qt::DecorationRole).value<QPixmap>();
QRect iconRect;
- if (!iconPixmap.isNull()) {
- QSize scaledSize = iconPixmap.size();
- scaledSize.scale(iconSize, Qt::KeepAspectRatio);
+ if (!icon_pixmap.isNull()) {
+ QSize scaled = icon_pixmap.size();
+ scaled.scale(icon_size, icon_size, Qt::KeepAspectRatio);
- int x = cardRect.left() + (cardRect.width() - scaledSize.width()) / 2;
- int y = cardRect.top() + margins;
-
- iconRect = QRect(x, y, scaledSize.width(), scaledSize.height());
+ iconRect = {cardRect.left() + (cardRect.width() - scaled.width()) / 2,
+ cardRect.top() + cardMargin, scaled.width(), scaled.height()};
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
- // Put this in a separate thing on the painter stack to prevent clipping the text.
painter->save();
-
- // round image edges
- QPainterPath path;
- path.addRoundedRect(iconRect, 10, 10);
- painter->setClipPath(path);
-
- painter->drawPixmap(iconRect, iconPixmap);
-
+ QPainterPath clip_path;
+ clip_path.addRoundedRect(iconRect, cardCornerRadius, cardCornerRadius);
+ painter->setClipPath(clip_path);
+ painter->drawPixmap(iconRect, icon_pixmap);
painter->restore();
} else {
- // if there is no icon just draw a blank rect
- iconRect = QRect(cardRect.left() + margins, cardRect.top() + margins, _iconsize, _iconsize);
+ iconRect = {cardRect.left() + cardMargin, cardRect.top() + cardMargin,
+ static_cast<int>(icon_size), static_cast<int>(icon_size)};
}
if (UISettings::values.show_game_name.GetValue()) {
- // padding + text
QRect textRect = cardRect;
- textRect.setTop(iconRect.bottom() + margins);
- textRect.adjust(margins, 0, -margins, -margins);
+ textRect.setTop(iconRect.bottom() + cardMargin);
+ textRect.adjust(cardMargin, 0, -cardMargin, -cardMargin);
- // We are already crammed on space, ignore the row 2
- QString title = index.data(Qt::DisplayRole).toString();
- title = title.split(QLatin1Char('\n')).first();
+ QString title = index.data(Qt::DisplayRole).toString().split(QLatin1Char('\n')).first();
- // now draw text
painter->setPen(textColor);
QFont font = option.font;
font.setBold(true);
-
- // TODO(crueter): fix this abysmal scaling
- font.setPixelSize(1.5 + std::max(10.0, std::sqrt(_iconsize)));
-
- // TODO(crueter): elide mode
+ font.setPixelSize(std::max(11.0, std::sqrt(static_cast<double>(icon_size))));
painter->setFont(font);
painter->drawText(textRect, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, title);
diff --git a/src/yuzu/game/game_grid.cpp b/src/yuzu/game/game_grid.cpp
new file mode 100644
index 0000000000..88e4a205a8
--- /dev/null
+++ b/src/yuzu/game/game_grid.cpp
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <QScroller>
+#include <QScrollerProperties>
+
+#include "qt_common/config/uisettings.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);
+ setItemDelegate(m_gameCard);
+
+ setViewMode(QListView::ListMode);
+ setResizeMode(QListView::Fixed);
+ setUniformItemSizes(true);
+ setSelectionMode(QAbstractItemView::SingleSelection);
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
+
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ setEditTriggers(QAbstractItemView::NoEditTriggers);
+ setContextMenuPolicy(Qt::CustomContextMenu);
+ setGridSize(QSize(140, 160));
+ m_gameCard->setSize(gridSize(), 0, 4);
+
+ setSpacing(10);
+ setWordWrap(true);
+ setTextElideMode(Qt::ElideRight);
+ setFlow(QListView::LeftToRight);
+ setWrapping(true);
+}
+
+void GameGrid::SetModel(GameListModel* model) {
+ QListView::setModel(model);
+ UpdateIconSize();
+}
+
+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)) {
+ setRowHidden(i, false);
+ } else {
+ setRowHidden(i, true);
+ }
+ }
+}
+
+void GameGrid::UpdateIconSize() {
+ const u32 icon_size = UISettings::values.game_icon_size.GetValue();
+
+ int heightMargin = 0;
+ int widthMargin = 80;
+
+ if (UISettings::values.show_game_name) {
+ switch (icon_size) {
+ case 128:
+ heightMargin = 65;
+ break;
+ case 0:
+ widthMargin = 120;
+ heightMargin = 120;
+ break;
+ case 64:
+ heightMargin = 77;
+ break;
+ case 32:
+ case 256:
+ heightMargin = 81;
+ break;
+ }
+ } else {
+ widthMargin = 24;
+ heightMargin = 24;
+ }
+
+ const int view_width = viewport()->width();
+
+ const double spacing = 0.01;
+ const int min_item_width = icon_size + widthMargin;
+
+ int columns = std::max(1, (view_width - 16) / min_item_width);
+ int stretched_width = ((view_width) - (spacing * (columns - 1))) / columns;
+
+ QSize grid_size(stretched_width, icon_size + heightMargin);
+ if (gridSize() != grid_size) {
+ setUpdatesEnabled(false);
+
+ setGridSize(grid_size);
+ m_gameCard->setSize(grid_size, stretched_width - min_item_width, columns);
+
+ setUpdatesEnabled(true);
+ }
+}
diff --git a/src/yuzu/game/game_grid.h b/src/yuzu/game/game_grid.h
new file mode 100644
index 0000000000..b670bfc3cb
--- /dev/null
+++ b/src/yuzu/game/game_grid.h
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QListView>
+#include <QString>
+
+#include "common/common_types.h"
+
+class GameCard;
+class GameListModel;
+
+class GameGrid : public QListView {
+ Q_OBJECT
+
+public:
+ explicit GameGrid(QWidget* parent = nullptr);
+
+ void SetModel(GameListModel* model);
+ void ApplyFilter(const QString& edit_filter_text, GameListModel* model);
+ void UpdateIconSize();
+
+private:
+ GameCard* m_gameCard = nullptr;
+};
diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp
index b5967bacca..63267ddec6 100644
--- a/src/yuzu/game/game_list.cpp
+++ b/src/yuzu/game/game_list.cpp
@@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2015 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
#include <QAbstractItemView>
#include <QApplication>
#include <QDir>
@@ -15,431 +18,60 @@
#include <QScrollBar>
#include <QScroller>
#include <QScrollerProperties>
-#include <QThreadPool>
#include <QToolButton>
#include <QVariantAnimation>
-#include <fmt/ranges.h>
-#include <qnamespace.h>
+
#include "common/common_types.h"
#include "common/logging.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/qt_common.h"
#include "qt_common/util/game.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/game/game_list.h"
-#include "yuzu/game/game_list_p.h"
-#include "yuzu/game/game_list_worker.h"
+#include "qt_common/game_list/game_list_p.h"
+#include "yuzu/game/game_grid.h"
+#include "yuzu/game/game_tree.h"
+#include "qt_common/game_list/model.h"
#include "yuzu/main_window.h"
#include "yuzu/util/controller_navigation.h"
-
-GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent)
- : QObject(parent), gamelist{gamelist_} {}
-
-// EventFilter in order to process systemkeys while editing the searchfield
-bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* event) {
- // If it isn't a KeyRelease event then continue with standard event processing
- if (event->type() != QEvent::KeyRelease)
- return QObject::eventFilter(obj, event);
-
- QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
- QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
-
- // If the searchfield's text hasn't changed special function keys get checked
- // If no function key changes the searchfield's text the filter doesn't need to get reloaded
- if (edit_filter_text == edit_filter_text_old) {
- switch (keyEvent->key()) {
- // Escape: Resets the searchfield
- case Qt::Key_Escape: {
- if (edit_filter_text_old.isEmpty()) {
- return QObject::eventFilter(obj, event);
- } else {
- gamelist->search_field->edit_filter->clear();
- edit_filter_text.clear();
- }
- break;
- }
- // Return and Enter
- // If the enter key gets pressed first checks how many and which entry is visible
- // If there is only one result launch this game
- case Qt::Key_Return:
- case Qt::Key_Enter: {
- if (gamelist->search_field->visible == 1) {
- const QString file_path = gamelist->GetLastFilterResultItem();
-
- // To avoid loading error dialog loops while confirming them using enter
- // Also users usually want to run a different game after closing one
- gamelist->search_field->edit_filter->clear();
- edit_filter_text.clear();
- emit gamelist->GameChosen(file_path);
- } else {
- return QObject::eventFilter(obj, event);
- }
- break;
- }
- default:
- return QObject::eventFilter(obj, event);
- }
- }
- edit_filter_text_old = edit_filter_text;
- return QObject::eventFilter(obj, event);
-}
-
-void GameListSearchField::setFilterResult(int visible_, int total_) {
- visible = visible_;
- total = total_;
-
- label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
-}
-
-QString GameListSearchField::filterText() const {
- return edit_filter->text();
-}
-
-QString GameList::GetLastFilterResultItem() const {
- QString file_path;
-
- for (int i = 1; i < item_model->rowCount() - 1; ++i) {
- const QStandardItem* 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) {
- if (tree_view->isRowHidden(j, folder_index)) {
- continue;
- }
-
- const QStandardItem* child = folder->child(j, 0);
- file_path = child->data(GameListItemPath::FullPathRole).toString();
- }
- }
-
- return file_path;
-}
-
-void GameListSearchField::clear() {
- edit_filter->clear();
-}
-
-void GameListSearchField::setFocus() {
- if (edit_filter->isVisible()) {
- edit_filter->setFocus();
- }
-}
-
-GameListSearchField::GameListSearchField(GameList* parent) : QWidget{parent} {
- auto* const key_release_eater = new KeyReleaseEater(parent, this);
- layout_filter = new QHBoxLayout;
- layout_filter->setContentsMargins(8, 8, 8, 8);
- label_filter = new QLabel;
- edit_filter = new QLineEdit;
- edit_filter->clear();
- edit_filter->installEventFilter(key_release_eater);
- edit_filter->setClearButtonEnabled(true);
- connect(edit_filter, &QLineEdit::textChanged, parent, &GameList::OnTextChanged);
- label_filter_result = new QLabel;
- button_filter_close = new QToolButton(this);
- button_filter_close->setText(QStringLiteral("X"));
- button_filter_close->setCursor(Qt::ArrowCursor);
- button_filter_close->setStyleSheet(
- QStringLiteral("QToolButton{ border: none; padding: 0px; color: "
- "#000000; font-weight: bold; background: #F0F0F0; }"
- "QToolButton:hover{ border: none; padding: 0px; color: "
- "#EEEEEE; font-weight: bold; background: #E81123}"));
- connect(button_filter_close, &QToolButton::clicked, parent, &GameList::OnFilterCloseClicked);
- layout_filter->setSpacing(10);
- layout_filter->addWidget(label_filter);
- layout_filter->addWidget(edit_filter);
- layout_filter->addWidget(label_filter_result);
- layout_filter->addWidget(button_filter_close);
- setLayout(layout_filter);
- RetranslateUI();
-}
-
-/**
- * Checks if all words separated by spaces are contained in another string
- * This offers a word order insensitive search function
- *
- * @param haystack String that gets checked if it contains all words of the userinput string
- * @param userinput String containing all words getting checked
- * @return true if the haystack contains all words of userinput
- */
-static 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); });
-}
-
-// Syncs the expanded state of Game Directories with settings to persist across sessions
-void GameList::OnItemExpanded(const QModelIndex& item) {
- const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>();
- const bool is_dir = type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
- type == GameListItemType::UserNandDir ||
- type == GameListItemType::SysNandDir;
- const bool is_fave = type == GameListItemType::Favorites;
- if (!is_dir && !is_fave) {
- return;
- }
- const bool is_expanded = tree_view->isExpanded(item);
- if (is_fave) {
- UISettings::values.favorites_expanded = is_expanded;
- return;
- }
- const int item_dir_index = item.data(GameListDir::GameDirRole).toInt();
- UISettings::values.game_dirs[item_dir_index].expanded = is_expanded;
-}
-
-// Event in order to filter the gamelist after editing the searchfield
-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
-
- // 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;
- hide(j, false, folder_index);
- }
- }
- search_field->setFilterResult(children_total, children_total);
- } else {
- 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();
- const int children_count = folder->rowCount();
- for (int j = 0; j < children_count; ++j) {
- ++children_total;
-
- 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'});
-
- // Only items which filename in combination with its title contains all words
- // that are in the searchfield will be visible in the gamelist
- // The search is case insensitive because of toLower()
- // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
- // multiple conversions of edit_filter_text for each game in the gamelist
- const QString file_name =
- file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
- file_title;
- if (ContainsAllWords(file_name, edit_filter_text) ||
- (file_program_id.size() == 16 && file_program_id.contains(edit_filter_text))) {
- hide(j, false, folder_index);
- ++result_count;
- } else {
- hide(j, true, folder_index);
- }
- }
- }
- search_field->setFilterResult(result_count, children_total);
- }
-}
-
-void GameList::OnUpdateThemedIcons() {
- for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
- QStandardItem* child = item_model->invisibleRootItem()->child(i);
-
- const int icon_size = UISettings::values.folder_icon_size.GetValue();
-
- switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
- 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:
- child->setData(
- QIcon::fromTheme(QStringLiteral("chip"))
- .pixmap(icon_size)
- .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
- Qt::DecorationRole);
- break;
- 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 GameList::OnFilterCloseClicked() {
- main_window->filterBarSetChecked(false);
-}
+#include "yuzu/game/search_field.h"
GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_,
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
MainWindow* parent)
: QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
play_time_manager{play_time_manager_}, system{system_} {
- watcher = new QFileSystemWatcher(this);
- connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
-
- external_watcher = new QFileSystemWatcher(this);
- ResetExternalWatcher();
- connect(external_watcher, &QFileSystemWatcher::directoryChanged, this,
- &GameList::RefreshExternalContent);
this->main_window = parent;
layout = new QVBoxLayout;
- tree_view = new QTreeView(this);
- list_view = new QListView(this);
- m_gameCard = new GameCard(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
+ setLayout(layout);
- list_view->setItemDelegate(m_gameCard);
+ item_model = new GameListModel(vfs, provider, play_time_manager, system, this);
- 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);
+ SetupViews();
- SetupScrollAnimation();
+ search_field = new GameListSearchField(this);
+ layout->addWidget(search_field);
- // tree
- tree_view->setAlternatingRowColors(true);
- tree_view->setSelectionMode(QHeaderView::SingleSelection);
- tree_view->setSelectionBehavior(QHeaderView::SelectRows);
- tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
- tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
- tree_view->setSortingEnabled(true);
- tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
- tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
- tree_view->setAttribute(Qt::WA_AcceptTouchEvents, true);
- tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
-
- // list view setup
- list_view->setViewMode(QListView::ListMode);
- list_view->setResizeMode(QListView::Fixed);
- list_view->setUniformItemSizes(true);
- list_view->setSelectionMode(QAbstractItemView::SingleSelection);
- list_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
- list_view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
-
- // Forcefully disable scroll bar, prevents thing where game list items
- // will start clamping prematurely.
- list_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
- list_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
- list_view->setContextMenuPolicy(Qt::CustomContextMenu);
- list_view->setGridSize(QSize(140, 160));
- m_gameCard->setSize(list_view->gridSize(), 0, 4);
-
- 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();
+ controller_navigation = new ControllerNavigation(system.HIDCore(), this);
- tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
- tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
- tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
- item_model->setSortRole(GameListItemPath::SortRole);
+ 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);
- connect(list_view, &QListView::activated, this, &GameList::ValidateEntry);
- connect(list_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
+ connect(grid_view, &QListView::activated, this, &GameList::ValidateEntry);
+ connect(grid_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,
[this](Qt::Key key) {
- // Avoid pressing buttons while playing
if (system.IsPoweredOn()) {
return;
}
@@ -447,31 +79,90 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
return;
}
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
-
QCoreApplication::postEvent(m_currentView, event);
});
- // We must register all custom types with the Qt Automoc system so that we are able to use
- // it with signals/slots. In this case, QList falls under the umbrella of custom types.
+ connect(item_model, &GameListModel::PopulatingCompleted, this,
+ &GameList::OnPopulatingCompleted);
+
+ connect(item_model, &GameListModel::ShowList, this, &GameList::ShowList);
+ connect(item_model, &GameListModel::SaveConfig, this, &GameList::SaveConfig);
+
+ connect(tree_view, &GameTree::FilterResultReady, search_field,
+ [this](int visible, int total) { search_field->setFilterResult(visible, total); });
+
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
- layout->setContentsMargins(0, 0, 0, 0);
- layout->setSpacing(0);
+ ResetViewMode();
+}
+
+GameList::~GameList() {
+ UnloadController();
+}
+
+void GameList::SetupViews() {
+ tree_view = new GameTree(this);
+ grid_view = new GameGrid(this);
+
+ tree_view->SetModel(item_model);
+ grid_view->SetModel(item_model);
layout->addWidget(tree_view);
- layout->addWidget(list_view);
- layout->addWidget(search_field);
- setLayout(layout);
+ layout->addWidget(grid_view);
+}
- ResetViewMode();
+QString GameList::GetLastFilterResultItem() const {
+ return tree_view->GetLastFilterResultItem();
}
-void GameList::UnloadController() {
- controller_navigation->UnloadController();
+void GameList::ClearFilter() {
+ search_field->clear();
+}
+
+void GameList::SetFilterFocus() {
+ if (item_model->rowCount() > 0) {
+ search_field->setFocus();
+ }
}
-bool GameList::IsTreeMode() {
- return m_isTreeMode;
+void GameList::SetFilterVisible(bool visibility) {
+ search_field->setVisible(visibility);
+}
+
+bool GameList::IsEmpty() const {
+ return item_model->IsEmpty();
+}
+
+void GameList::LoadCompatibilityList() {
+ item_model->LoadCompatibilityList();
+}
+
+void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
+ m_currentView->setEnabled(false);
+
+ tree_view->UpdateColumnVisibility(item_model);
+
+ if (!m_isTreeMode) {
+ grid_view->UpdateIconSize();
+ }
+
+ item_model->PopulateAsync(game_dirs);
+}
+
+void GameList::SaveInterfaceLayout() {
+ tree_view->SaveInterfaceLayout();
+}
+
+void GameList::LoadInterfaceLayout() {
+ tree_view->LoadInterfaceLayout();
+}
+
+QStandardItemModel* GameList::GetModel() const {
+ return item_model;
+}
+
+void GameList::UnloadController() {
+ controller_navigation->UnloadController();
}
void GameList::ResetViewMode() {
@@ -482,15 +173,13 @@ void GameList::ResetViewMode() {
case Settings::GameListMode::TreeView:
m_currentView = tree_view;
newTreeMode = true;
-
tree_view->setVisible(true);
- list_view->setVisible(false);
+ grid_view->setVisible(false);
break;
case Settings::GameListMode::GridView:
- m_currentView = list_view;
+ m_currentView = grid_view;
newTreeMode = false;
-
- list_view->setVisible(true);
+ grid_view->setVisible(true);
tree_view->setVisible(false);
break;
default:
@@ -500,11 +189,9 @@ void GameList::ResetViewMode() {
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);
@@ -517,133 +204,70 @@ void GameList::ResetViewMode() {
if (m_isTreeMode != newTreeMode) {
m_isTreeMode = newTreeMode;
-
- RefreshGameDirectory();
- }
-}
-
-GameList::~GameList() {
- UnloadController();
-}
-
-void GameList::SetFilterFocus() {
- if (tree_view->model()->rowCount() > 0) {
- search_field->setFocus();
+ item_model->SetFlat(!m_isTreeMode);
+ if (!UISettings::values.game_dirs.empty()) {
+ item_model->PopulateAsync(UISettings::values.game_dirs);
+ }
}
}
-void GameList::SetFilterVisible(bool visibility) {
- search_field->setVisible(visibility);
-}
-
-void GameList::ClearFilter() {
- search_field->clear();
-}
-
-void GameList::WorkerEvent() {
- current_worker->ProcessEvents(this);
-}
+void GameList::OnTextChanged(const QString& new_text) {
+ const QString edit_filter_text = new_text.toLower();
-void GameList::AddDirEntry(GameListDir* entry_items) {
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);
+ tree_view->ApplyFilter(edit_filter_text, item_model);
+ } else {
+ grid_view->ApplyFilter(edit_filter_text, item_model);
}
}
-void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
- if (!m_isTreeMode)
- item_model->invisibleRootItem()->appendRow(entry_items);
- else
- parent->appendRow(entry_items);
-}
-
-void GameList::ValidateEntry(const QModelIndex& item) {
- const auto selected = item.sibling(item.row(), 0);
-
- switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
- case GameListItemType::Game: {
- const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
- if (file_path.isEmpty())
- return;
- const QFileInfo file_info(file_path);
- if (!file_info.exists())
- return;
-
- if (file_info.isDir()) {
- const QDir dir{file_path};
- const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
- if (matching_main.size() == 1) {
- emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
- }
- return;
- }
-
- const auto title_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong();
-
- // Users usually want to run a different game after closing one
- search_field->clear();
- emit GameChosen(file_path, title_id);
- break;
- }
- case GameListItemType::AddDir:
- emit AddDirectory();
- break;
- default:
- break;
- }
+void GameList::OnFilterCloseClicked() {
+ main_window->filterBarSetChecked(false);
}
-bool GameList::IsEmpty() const {
- for (int i = 0; i < item_model->rowCount(); i++) {
- const QStandardItem* child = item_model->invisibleRootItem()->child(i);
- const auto type = static_cast<GameListItemType>(child->type());
-
- if (!child->hasChildren() &&
- (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
- type == GameListItemType::SysNandDir)) {
- item_model->invisibleRootItem()->removeRow(child->row());
- i--;
- }
- }
-
- return !item_model->invisibleRootItem()->hasChildren();
+void GameList::OnUpdateThemedIcons() {
+ item_model->OnUpdateThemedIcons();
}
-void GameList::DonePopulating(const QStringList& watch_list) {
- emit ShowList(!IsEmpty());
+void GameList::OnPopulatingCompleted(const QStringList& watch_list) {
+ emit ShowList(!item_model->IsEmpty());
- // Add favorites row
+ // favorites row
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);
+ }
+
+ // restore collapsed/expanded flags
+ if (m_isTreeMode) {
+ const auto* root = item_model->invisibleRootItem();
+ for (int i = 1; i < root->rowCount() - 1; ++i) {
+ const auto* dir_item = root->child(i);
+ const auto type = dir_item->data(GameListItem::TypeRole).value<GameListItemType>();
+ if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
+ type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir) {
+ const int dir_index = dir_item->data(GameListDir::GameDirRole).toInt();
+ if (dir_index >= 0 && dir_index < UISettings::values.game_dirs.size()) {
+ tree_view->setExpanded(dir_item->index(),
+ UISettings::values.game_dirs[dir_index].expanded);
+ }
+ }
}
}
// Clear out the old directories to watch for changes and add the new ones
+ auto* watcher = item_model->GetWatcher();
auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) {
watcher->removePaths(watch_dirs);
}
- // Workaround: Add the watch paths in chunks to allow the gui to refresh
- // This prevents the UI from stalling when a large number of watch paths are added
- // Also artificially caps the watcher to a certain number of directories
+
constexpr int LIMIT_WATCH_DIRECTORIES = 5000;
constexpr int SLICE_SIZE = 25;
int len = (std::min)(static_cast<int>(watch_list.size()), LIMIT_WATCH_DIRECTORIES);
- // Block signals to prevent the watcher from triggering a refresh while we are adding paths.
- // This fixes a refresh loop on macOS.
#ifdef __APPLE__
const bool old_signals_blocked = watcher->blockSignals(true);
#endif
@@ -659,6 +283,7 @@ void GameList::DonePopulating(const QStringList& watch_list) {
#ifdef __APPLE__
watcher->blockSignals(old_signals_blocked);
#endif
+
m_currentView->setEnabled(true);
int children_total = 0;
@@ -669,12 +294,82 @@ void GameList::DonePopulating(const QStringList& watch_list) {
if (children_total > 0) {
search_field->setFocus();
}
+
item_model->sort(tree_view->header()->sortIndicatorSection(),
tree_view->header()->sortIndicatorOrder());
emit PopulatingCompleted();
}
+void GameList::RefreshGameDirectory() {
+ item_model->ResetExternalWatcher();
+
+ if (!UISettings::values.game_dirs.empty()) {
+ LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
+ QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
+ PopulateAsync(UISettings::values.game_dirs);
+ }
+}
+
+void GameList::RefreshExternalContent() {
+ if (!UISettings::values.game_dirs.empty()) {
+ LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache.");
+ QtCommon::Game::ResetMetadata(false);
+ QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
+ PopulateAsync(UISettings::values.game_dirs);
+ }
+}
+
+void GameList::ValidateEntry(const QModelIndex& item) {
+ const auto selected = item.sibling(item.row(), 0);
+
+ switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
+ case GameListItemType::Game: {
+ const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
+ if (file_path.isEmpty())
+ return;
+ const QFileInfo file_info(file_path);
+ if (!file_info.exists())
+ return;
+
+ if (file_info.isDir()) {
+ const QDir dir{file_path};
+ const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
+ if (matching_main.size() == 1) {
+ emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
+ }
+ return;
+ }
+
+ const auto title_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong();
+
+ search_field->clear();
+ emit GameChosen(file_path, title_id);
+ break;
+ }
+ case GameListItemType::AddDir:
+ emit AddDirectory();
+ break;
+ default:
+ break;
+ }
+}
+
+void GameList::ToggleFavorite(u64 program_id) {
+ item_model->ToggleFavorite(program_id);
+
+ if (UISettings::values.favorited_ids.contains(program_id)) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ !search_field->filterText().isEmpty());
+ item_model->sort(tree_view->header()->sortIndicatorSection(),
+ tree_view->header()->sortIndicatorOrder());
+ } else {
+ if (UISettings::values.favorited_ids.size() == 0) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+ }
+ }
+}
+
void GameList::PopupContextMenu(const QPoint& menu_location) {
QModelIndex item = m_currentView->indexAt(menu_location);
if (!item.isValid()) {
@@ -747,7 +442,6 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
-// TODO: Implement shortcut creation for macOS
#if !defined(__APPLE__)
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
@@ -769,8 +463,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
remove_vk_shader_cache->setVisible(program_id != 0);
remove_shader_cache->setVisible(program_id != 0);
remove_all_content->setVisible(program_id != 0);
- auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
- navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
+ auto& compat_list = item_model->GetCompatibilityList();
+ auto it = FindMatchingCompatibilityEntry(compat_list, program_id);
+ navigate_to_gamedb_entry->setVisible(it != compat_list.end() && program_id != 0);
connect(favorite, &QAction::triggered, this,
[this, program_id]() { ToggleFavorite(program_id); });
@@ -779,8 +474,9 @@ 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);
});
@@ -831,9 +527,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(copy_tid, &QAction::triggered, this,
[this, program_id]() { emit CopyTIDRequested(program_id); });
connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() {
- emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
+ emit NavigateToGamedbEntryRequested(program_id, item_model->GetCompatibilityList());
});
-// TODO: Implement shortcut creation for macOS
#if !defined(__APPLE__)
connect(create_desktop_shortcut, &QAction::triggered, this, [this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Desktop);
@@ -885,32 +580,26 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
connect(move_up, &QAction::triggered, this, [this, selected, row, game_dir_index] {
const int other_index = selected.sibling(row - 1, 0).data(GameListDir::GameDirRole).toInt();
- // swap the items in the settings
std::swap(UISettings::values.game_dirs[game_dir_index],
UISettings::values.game_dirs[other_index]);
- // swap the indexes held by the QVariants
item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole);
item_model->setData(selected.sibling(row - 1, 0), QVariant(game_dir_index),
GameListDir::GameDirRole);
- // move the treeview items
- QList<QStandardItem*> item = item_model->takeRow(row);
- item_model->invisibleRootItem()->insertRow(row - 1, item);
+ QList<QStandardItem*> items = item_model->takeRow(row);
+ item_model->invisibleRootItem()->insertRow(row - 1, items);
tree_view->setExpanded(selected.sibling(row - 1, 0),
UISettings::values.game_dirs[other_index].expanded);
});
connect(move_down, &QAction::triggered, this, [this, selected, row, game_dir_index] {
const int other_index = selected.sibling(row + 1, 0).data(GameListDir::GameDirRole).toInt();
- // swap the items in the settings
std::swap(UISettings::values.game_dirs[game_dir_index],
UISettings::values.game_dirs[other_index]);
- // swap the indexes held by the QVariants
item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole);
item_model->setData(selected.sibling(row + 1, 0), QVariant(game_dir_index),
GameListDir::GameDirRole);
- // move the treeview items
- const QList<QStandardItem*> item = item_model->takeRow(row);
- item_model->invisibleRootItem()->insertRow(row + 1, item);
+ const QList<QStandardItem*> items = item_model->takeRow(row);
+ item_model->invisibleRootItem()->insertRow(row + 1, items);
tree_view->setExpanded(selected.sibling(row + 1, 0),
UISettings::values.game_dirs[other_index].expanded);
});
@@ -925,58 +614,12 @@ void GameList::AddFavoritesPopup(QMenu& context_menu) {
QAction* clear = context_menu.addAction(tr("Clear"));
connect(clear, &QAction::triggered, this, [this] {
- for (const auto id : std::as_const(UISettings::values.favorited_ids)) {
- RemoveFavorite(id);
- }
UISettings::values.favorited_ids.clear();
+ item_model->invisibleRootItem()->child(0)->removeRows(0, item_model->invisibleRootItem()->child(0)->rowCount());
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
});
}
-void GameList::LoadCompatibilityList() {
- QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")};
-
- if (!compat_list.open(QFile::ReadOnly | QFile::Text)) {
- LOG_ERROR(Frontend, "Unable to open game compatibility list");
- return;
- }
-
- if (compat_list.size() == 0) {
- LOG_WARNING(Frontend, "Game compatibility list is empty");
- return;
- }
-
- const QByteArray content = compat_list.readAll();
- if (content.isEmpty()) {
- LOG_ERROR(Frontend, "Unable to completely read game compatibility list");
- return;
- }
-
- const QJsonDocument json = QJsonDocument::fromJson(content);
- const QJsonArray arr = json.array();
-
- for (const QJsonValue& value : arr) {
- const QJsonObject game = value.toObject();
- const QString compatibility_key = QStringLiteral("compatibility");
-
- if (!game.contains(compatibility_key) || !game[compatibility_key].isDouble()) {
- continue;
- }
-
- const int compatibility = game[compatibility_key].toInt();
- const QString directory = game[QStringLiteral("directory")].toString();
- const QJsonArray ids = game[QStringLiteral("releases")].toArray();
-
- for (const QJsonValue& id_ref : ids) {
- const QJsonObject id_object = id_ref.toObject();
- const QString id = id_object[QStringLiteral("id")].toString();
-
- compatibility_list.emplace(id.toUpper().toStdString(),
- std::make_pair(QString::number(compatibility), directory));
- }
- }
-}
-
void GameList::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) {
RetranslateUI();
@@ -986,254 +629,11 @@ void GameList::changeEvent(QEvent* event) {
}
void GameList::RetranslateUI() {
- item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name"));
- item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, tr("Compatibility"));
- item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
- item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
- item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
- item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
-}
-
-void GameListSearchField::changeEvent(QEvent* event) {
- if (event->type() == QEvent::LanguageChange) {
- RetranslateUI();
- }
-
- QWidget::changeEvent(event);
-}
-
-void GameListSearchField::RetranslateUI() {
- label_filter->setText(tr("Filter:"));
- edit_filter->setPlaceholderText(tr("Enter pattern to filter"));
-}
-
-QStandardItemModel* GameList::GetModel() const {
- return item_model;
-}
-
-void GameList::UpdateIconSize() {
- // Update sizes and stuff for the list view
- const u32 icon_size = UISettings::values.game_icon_size.GetValue();
-
- int heightMargin = 0;
- int widthMargin = 80;
-
- if (UISettings::values.show_game_name) {
- // the scaling on the card is kinda abysmal.
- // TODO(crueter): refactor
- switch (icon_size) {
- case 128:
- heightMargin = 65;
- break;
- case 0:
- widthMargin = 120;
- heightMargin = 120;
- break;
- case 64:
- heightMargin = 77;
- break;
- case 32:
- case 256:
- heightMargin = 81;
- break;
- }
- } else {
- widthMargin = 24;
- heightMargin = 24;
- }
-
- // "auto" resize //
- const int view_width = list_view->viewport()->width();
-
- // Tiny space padding to prevent the list view from forcing its own resize operation.
- const double spacing = 0.01;
- const int min_item_width = icon_size + widthMargin;
-
- // And now stretch it a bit to fill out remaining space.
- // Not perfect but works well enough for now
- int columns = std::max(1, (view_width - 16) / min_item_width);
- int stretched_width = ((view_width) - (spacing * (columns - 1))) / columns;
-
- // only updates things if grid size is changed
- QSize grid_size(stretched_width, icon_size + heightMargin);
- if (list_view->gridSize() != grid_size) {
- list_view->setUpdatesEnabled(false);
-
- list_view->setGridSize(grid_size);
- m_gameCard->setSize(grid_size, stretched_width - min_item_width, columns);
-
- list_view->setUpdatesEnabled(true);
- }
-}
-
-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);
- tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
- tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
- tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
- tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
-
- if (!m_isTreeMode)
- UpdateIconSize();
-
- // Cancel any existing worker.
- current_worker.reset();
-
- // Delete any rows that might already exist if we're repopulating
- 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);
-
- // Get events from the worker as data becomes available
- connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
- Qt::QueuedConnection);
-
- QThreadPool::globalInstance()->start(current_worker.get());
-}
-
-void GameList::SaveInterfaceLayout() {
- UISettings::values.gamelist_header_state = tree_view->header()->saveState();
-}
-
-void GameList::LoadInterfaceLayout() {
- auto* header = tree_view->header();
-
- if (header->restoreState(UISettings::values.gamelist_header_state))
- return;
-
- // We are using the name column to display icons and titles
- // so make it as large as possible as default.
-
- // TODO(crueter): width() is not initialized yet, so use a sane default value
- header->resizeSection(COLUMN_NAME, 840);
-}
-
-void GameList::RefreshGameDirectory() {
- // Reset the externals watcher whenever the game list is reloaded,
- // primarily ensures that new titles and external dirs are caught.
- ResetExternalWatcher();
-
- if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
- LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
- QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
- PopulateAsync(UISettings::values.game_dirs);
- }
-}
-
-void GameList::RefreshExternalContent() {
- // TODO: Explore the possibility of only resetting the metadata cache for that specific game.
- if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
- LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache.");
- QtCommon::Game::ResetMetadata(false);
- QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
- PopulateAsync(UISettings::values.game_dirs);
- }
-}
-
-void GameList::ResetExternalWatcher() {
- auto watch_dirs = external_watcher->directories();
- if (!watch_dirs.isEmpty()) {
- external_watcher->removePaths(watch_dirs);
- }
-
- for (const std::string& dir : Settings::values.external_content_dirs) {
- external_watcher->addPath(QString::fromStdString(dir));
- }
-}
-
-void GameList::ToggleFavorite(u64 program_id) {
- if (!UISettings::values.favorited_ids.contains(program_id)) {
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
- !search_field->filterText().isEmpty());
- UISettings::values.favorited_ids.append(program_id);
- AddFavorite(program_id);
- item_model->sort(tree_view->header()->sortIndicatorSection(),
- tree_view->header()->sortIndicatorOrder());
- } else {
- UISettings::values.favorited_ids.removeOne(program_id);
- RemoveFavorite(program_id);
- if (UISettings::values.favorited_ids.size() == 0) {
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
- }
- }
- emit SaveConfig();
-}
-
-void GameList::AddFavorite(u64 program_id) {
- auto* favorites_row = item_model->item(0);
-
- for (int i = 1; i < item_model->rowCount() - 1; i++) {
- const auto* folder = item_model->item(i);
- for (int j = 0; j < folder->rowCount(); j++) {
- if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
- program_id) {
- QList<QStandardItem*> list;
- for (int k = 0; k < COLUMN_COUNT; k++) {
- list.append(folder->child(j, k)->clone());
- }
- list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
- GameListItem::SortRole);
- list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
-
- favorites_row->appendRow(list);
- return;
- }
- }
- }
-}
-
-void GameList::RemoveFavorite(u64 program_id) {
- auto* favorites_row = item_model->item(0);
-
- for (int i = 0; i < favorites_row->rowCount(); i++) {
- const auto* game = favorites_row->child(i);
- if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
- favorites_row->removeRow(i);
- return;
- }
- }
-}
-
-GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} {
- connect(parent, &MainWindow::UpdateThemedIcons, this,
- &GameListPlaceholder::onUpdateThemedIcons);
-
- layout = new QVBoxLayout;
- image = new QLabel;
- text = new QLabel;
- layout->setAlignment(Qt::AlignCenter);
- image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
-
- RetranslateUI();
- QFont font = text->font();
- font.setPointSize(20);
- text->setFont(font);
- text->setAlignment(Qt::AlignHCenter);
- image->setAlignment(Qt::AlignHCenter);
-
- layout->addWidget(image);
- layout->addWidget(text);
- setLayout(layout);
-}
-
-GameListPlaceholder::~GameListPlaceholder() = default;
-
-void GameListPlaceholder::onUpdateThemedIcons() {
- image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
-}
-
-void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
- emit GameListPlaceholder::AddDirectory();
+ item_model->RetranslateUI();
}
void GameList::SetupScrollAnimation() {
auto setup = [this](QVariantAnimation* anim, QScrollBar* bar) {
- // animation handles moving the bar instead of Qt's built in crap
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->setDuration(200);
connect(anim, &QVariantAnimation::valueChanged, this,
@@ -1246,8 +646,8 @@ 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());
+ setup(vertical_scroll, grid_view->verticalScrollBar());
+ setup(horizontal_scroll, grid_view->horizontalScrollBar());
}
bool GameList::eventFilter(QObject* obj, QEvent* event) {
@@ -1259,13 +659,11 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
int deltaX = wheelEvent->angleDelta().x();
int deltaY = wheelEvent->angleDelta().y();
- // if shift is held do a horizontal scroll
if (horizontal && deltaY != 0 && deltaX == 0) {
deltaX = deltaY;
deltaY = 0;
}
- // TODO(crueter): dedup this
if (deltaY != 0) {
if (vertical_scroll->state() == QAbstractAnimation::Stopped)
vertical_scroll_target = m_currentView->verticalScrollBar()->value();
@@ -1300,7 +698,6 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_currentView->viewport() && event->type() == QEvent::MouseButtonPress) {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
- // if the user clicks outside of the list, deselect the current item.
QModelIndex index = m_currentView->indexAt(mouseEvent->pos());
if (!index.isValid()) {
m_currentView->selectionModel()->clearSelection();
@@ -1308,14 +705,46 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
}
}
- if (obj == list_view->viewport() && event->type() == QEvent::Resize) {
- UpdateIconSize();
+ if (obj == grid_view->viewport() && event->type() == QEvent::Resize) {
+ grid_view->UpdateIconSize();
return true;
}
return QWidget::eventFilter(obj, event);
}
+GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} {
+ connect(parent, &MainWindow::UpdateThemedIcons, this,
+ &GameListPlaceholder::onUpdateThemedIcons);
+
+ layout = new QVBoxLayout;
+ image = new QLabel;
+ text = new QLabel;
+ layout->setAlignment(Qt::AlignCenter);
+ image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
+
+ RetranslateUI();
+ QFont font = text->font();
+ font.setPointSize(20);
+ text->setFont(font);
+ text->setAlignment(Qt::AlignHCenter);
+ image->setAlignment(Qt::AlignHCenter);
+
+ layout->addWidget(image);
+ layout->addWidget(text);
+ setLayout(layout);
+}
+
+GameListPlaceholder::~GameListPlaceholder() = default;
+
+void GameListPlaceholder::onUpdateThemedIcons() {
+ image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
+}
+
+void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
+ emit GameListPlaceholder::AddDirectory();
+}
+
void GameListPlaceholder::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) {
RetranslateUI();
diff --git a/src/yuzu/game/game_list.h b/src/yuzu/game/game_list.h
index 6cf857c4e1..de0db789aa 100644
--- a/src/yuzu/game/game_list.h
+++ b/src/yuzu/game/game_list.h
@@ -6,14 +6,12 @@
#pragma once
-#include <QFileSystemWatcher>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QPushButton>
#include <QStandardItemModel>
#include <QString>
-#include <QTreeView>
#include <QVBoxLayout>
#include <QVector>
#include <QWidget>
@@ -28,21 +26,22 @@
class QVariantAnimation;
-class QListView;
-
class GameCard;
-namespace Core {
-class System;
-}
-
+class GameListModel;
+class GameTree;
+class GameGrid;
+class GameListSearchField;
class ControllerNavigation;
class GameListWorker;
-class GameListSearchField;
class GameListDir;
class MainWindow;
enum class AmLaunchType;
enum class StartGameType;
+namespace Core {
+class System;
+}
+
namespace FileSys {
class ManualContentProvider;
class VfsFilesystem;
@@ -62,16 +61,6 @@ class GameList : public QWidget {
Q_OBJECT
public:
- enum {
- COLUMN_NAME,
- COLUMN_FILE_TYPE,
- COLUMN_SIZE,
- COLUMN_PLAY_TIME,
- COLUMN_ADD_ONS,
- COLUMN_COMPATIBILITY,
- COLUMN_COUNT, // Number of columns
- };
-
explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
FileSys::ManualContentProvider* provider_,
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
@@ -95,13 +84,11 @@ public:
/// Disables events from the emulated controller
void UnloadController();
- bool IsTreeMode();
void ResetViewMode();
public slots:
void RefreshGameDirectory();
void RefreshExternalContent();
- void ResetExternalWatcher();
signals:
void BootGame(const QString& game_path, StartGameType type);
@@ -130,27 +117,20 @@ signals:
void SaveConfig();
private slots:
- void OnItemExpanded(const QModelIndex& item);
void OnTextChanged(const QString& new_text);
void OnFilterCloseClicked();
void OnUpdateThemedIcons();
-
- void UpdateIconSize();
+ void OnPopulatingCompleted(const QStringList& watch_list);
private:
- friend class GameListWorker;
- void WorkerEvent();
-
- void AddDirEntry(GameListDir* entry_items);
- void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
- void DonePopulating(const QStringList& watch_list);
+ void SetupViews();
+ void SetupScrollAnimation();
+ bool eventFilter(QObject* obj, QEvent* event) override;
+ void changeEvent(QEvent*) override;
-private:
void ValidateEntry(const QModelIndex& item);
void ToggleFavorite(u64 program_id);
- void AddFavorite(u64 program_id);
- void RemoveFavorite(u64 program_id);
void PopupContextMenu(const QPoint& menu_location);
void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
@@ -158,41 +138,32 @@ private:
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
void AddFavoritesPopup(QMenu& context_menu);
- void changeEvent(QEvent*) override;
void RetranslateUI();
+ friend class GameListSearchField;
+
std::shared_ptr<FileSys::VfsFilesystem> vfs;
FileSys::ManualContentProvider* provider;
- GameListSearchField* search_field;
+ GameListSearchField* search_field = nullptr;
MainWindow* main_window = nullptr;
QVBoxLayout* layout = nullptr;
- QTreeView* tree_view = nullptr;
- QListView* list_view = nullptr;
- GameCard* m_gameCard = nullptr;
+ GameTree* tree_view = nullptr;
+ GameGrid* grid_view = nullptr;
+ GameListModel* item_model = nullptr;
- QStandardItemModel* item_model = nullptr;
- std::unique_ptr<GameListWorker> current_worker;
- QFileSystemWatcher* watcher = nullptr;
- QFileSystemWatcher* external_watcher = nullptr;
ControllerNavigation* controller_navigation = nullptr;
- CompatibilityList compatibility_list;
QVariantAnimation* vertical_scroll = nullptr;
QVariantAnimation* horizontal_scroll = nullptr;
int vertical_scroll_target = 0;
int horizontal_scroll_target = 0;
- void SetupScrollAnimation();
- bool eventFilter(QObject* obj, QEvent* event) override;
-
- friend class GameListSearchField;
-
const PlayTime::PlayTimeManager& play_time_manager;
Core::System& system;
bool m_isTreeMode = true;
- QAbstractItemView* m_currentView = tree_view;
+ QAbstractItemView* m_currentView = nullptr;
};
class GameListPlaceholder : public QWidget {
diff --git a/src/yuzu/game/game_tree.cpp b/src/yuzu/game/game_tree.cpp
new file mode 100644
index 0000000000..f670d03dc6
--- /dev/null
+++ b/src/yuzu/game/game_tree.cpp
@@ -0,0 +1,173 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <QApplication>
+#include <QHeaderView>
+#include <QScroller>
+#include <QScrollerProperties>
+
+#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"
+
+GameTree::GameTree(QWidget* parent) : QTreeView{parent} {
+ setAlternatingRowColors(true);
+ setSelectionMode(QHeaderView::SingleSelection);
+ setSelectionBehavior(QHeaderView::SelectRows);
+ setVerticalScrollMode(QHeaderView::ScrollPerPixel);
+ setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
+ setSortingEnabled(true);
+ setEditTriggers(QHeaderView::NoEditTriggers);
+ setContextMenuPolicy(Qt::CustomContextMenu);
+ setAttribute(Qt::WA_AcceptTouchEvents, true);
+ setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
+
+ connect(this, &QTreeView::expanded, this, &GameTree::OnItemExpanded);
+ connect(this, &QTreeView::collapsed, this, &GameTree::OnItemExpanded);
+}
+
+void GameTree::SetModel(GameListModel* model) {
+ QTreeView::setModel(model);
+ LoadInterfaceLayout();
+ UpdateColumnVisibility(model);
+}
+
+void GameTree::OnItemExpanded(const QModelIndex& item) {
+ const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>();
+ const bool is_dir = type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
+ type == GameListItemType::UserNandDir ||
+ type == GameListItemType::SysNandDir;
+ const bool is_fave = type == GameListItemType::Favorites;
+ if (!is_dir && !is_fave) {
+ return;
+ }
+ const bool is_expanded = isExpanded(item);
+ if (is_fave) {
+ UISettings::values.favorites_expanded = is_expanded;
+ return;
+ }
+ const int item_dir_index = item.data(GameListDir::GameDirRole).toInt();
+ UISettings::values.game_dirs[item_dir_index].expanded = is_expanded;
+}
+
+void GameTree::SaveInterfaceLayout() {
+ UISettings::values.gamelist_header_state = header()->saveState();
+}
+
+void GameTree::LoadInterfaceLayout() {
+ auto* hdr = header();
+
+ if (hdr->restoreState(UISettings::values.gamelist_header_state))
+ return;
+
+ hdr->resizeSection(GameListModel::COLUMN_NAME, 840);
+}
+
+void GameTree::UpdateColumnVisibility(GameListModel* model) {
+ Q_UNUSED(model)
+ setColumnHidden(GameListModel::COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
+ setColumnHidden(GameListModel::COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
+ setColumnHidden(GameListModel::COLUMN_FILE_TYPE, !UISettings::values.show_types);
+ setColumnHidden(GameListModel::COLUMN_SIZE, !UISettings::values.show_size);
+ setColumnHidden(GameListModel::COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
+}
+
+QString GameTree::GetLastFilterResultItem() const {
+ QString file_path;
+
+ auto* model = qobject_cast<GameListModel*>(QTreeView::model());
+ if (!model)
+ return {};
+
+ for (int i = 1; i < model->rowCount() - 1; ++i) {
+ const QStandardItem* folder = model->item(i, 0);
+ const QModelIndex folder_index = folder->index();
+ const int children_count = folder->rowCount();
+
+ for (int j = 0; j < children_count; ++j) {
+ if (isRowHidden(j, folder_index)) {
+ continue;
+ }
+
+ const QStandardItem* child = folder->child(j, 0);
+ file_path = child->data(GameListItemPath::FullPathRole).toString();
+ }
+ }
+
+ return file_path;
+}
+
+int GameTree::FilterClosedResultCount(GameListModel* model) {
+ int children_total = 0;
+
+ auto hide_favorites_row = UISettings::values.favorited_ids.size() == 0;
+ setRowHidden(0, model->invisibleRootItem()->index(), hide_favorites_row);
+
+ for (int i = 1; i < model->rowCount() - 1; ++i) {
+ auto* folder = 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;
+ setRowHidden(j, folder_index, false);
+ }
+ }
+
+ return children_total;
+}
+
+void GameTree::ApplyFilter(const QString& edit_filter_text, GameListModel* model) {
+ int children_total = 0;
+ int result_count = 0;
+
+ if (edit_filter_text.isEmpty()) {
+ children_total = FilterClosedResultCount(model);
+ emit FilterResultReady(children_total, children_total);
+ return;
+ }
+
+ setRowHidden(0, model->invisibleRootItem()->index(), true);
+
+ for (int i = 1; i < model->rowCount() - 1; ++i) {
+ auto* folder = 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;
+
+ 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))) {
+ setRowHidden(j, folder_index, false);
+ ++result_count;
+ } else {
+ setRowHidden(j, folder_index, true);
+ }
+ }
+ }
+
+ emit FilterResultReady(result_count, children_total);
+}
diff --git a/src/yuzu/game/game_tree.h b/src/yuzu/game/game_tree.h
new file mode 100644
index 0000000000..b5eebdb9dd
--- /dev/null
+++ b/src/yuzu/game/game_tree.h
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QHeaderView>
+#include <QString>
+#include <QTreeView>
+
+class GameListModel;
+
+class GameTree : public QTreeView {
+ Q_OBJECT
+
+public:
+ explicit GameTree(QWidget* parent = nullptr);
+
+ void SetModel(GameListModel* model);
+
+ QString GetLastFilterResultItem() const;
+ int FilterClosedResultCount(GameListModel* model);
+ void ApplyFilter(const QString& edit_filter_text, GameListModel* model);
+
+ void SaveInterfaceLayout();
+ void LoadInterfaceLayout();
+
+ void UpdateColumnVisibility(GameListModel* model);
+
+signals:
+ void ItemExpandedChanged(const QModelIndex& item);
+ void FilterResultReady(int visible, int total);
+
+private slots:
+ void OnItemExpanded(const QModelIndex& item);
+};
diff --git a/src/yuzu/game/search_field.cpp b/src/yuzu/game/search_field.cpp
new file mode 100644
index 0000000000..c2d4893742
--- /dev/null
+++ b/src/yuzu/game/search_field.cpp
@@ -0,0 +1,124 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "game/game_list.h"
+#include "game/search_field.h"
+
+#include <QKeyEvent>
+#include <QToolButton>
+
+// TODO: Remove GameList dependence?
+GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent)
+ : QObject(parent), gamelist{gamelist_} {}
+
+// EventFilter in order to process systemkeys while editing the searchfield
+bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* event) {
+ // If it isn't a KeyRelease event then continue with standard event processing
+ if (event->type() != QEvent::KeyRelease)
+ return QObject::eventFilter(obj, event);
+
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
+
+ // If the searchfield's text hasn't changed special function keys get checked
+ // If no function key changes the searchfield's text the filter doesn't need to get reloaded
+ if (edit_filter_text == edit_filter_text_old) {
+ switch (keyEvent->key()) {
+ // Escape: Resets the searchfield
+ case Qt::Key_Escape: {
+ if (edit_filter_text_old.isEmpty()) {
+ return QObject::eventFilter(obj, event);
+ } else {
+ gamelist->search_field->edit_filter->clear();
+ edit_filter_text.clear();
+ }
+ break;
+ }
+ // Return and Enter
+ // If the enter key gets pressed first checks how many and which entry is visible
+ // If there is only one result launch this game
+ case Qt::Key_Return:
+ case Qt::Key_Enter: {
+ if (gamelist->search_field->visible == 1) {
+ const QString file_path = gamelist->GetLastFilterResultItem();
+
+ // To avoid loading error dialog loops while confirming them using enter
+ // Also users usually want to run a different game after closing one
+ gamelist->search_field->edit_filter->clear();
+ edit_filter_text.clear();
+ emit gamelist->GameChosen(file_path);
+ } else {
+ return QObject::eventFilter(obj, event);
+ }
+ break;
+ }
+ default:
+ return QObject::eventFilter(obj, event);
+ }
+ }
+ edit_filter_text_old = edit_filter_text;
+ return QObject::eventFilter(obj, event);
+}
+
+void GameListSearchField::setFilterResult(int visible_, int total_) {
+ visible = visible_;
+ total = total_;
+
+ label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
+}
+
+QString GameListSearchField::filterText() const {
+ return edit_filter->text();
+}
+
+void GameListSearchField::clear() {
+ edit_filter->clear();
+}
+
+void GameListSearchField::setFocus() {
+ if (edit_filter->isVisible()) {
+ edit_filter->setFocus();
+ }
+}
+
+GameListSearchField::GameListSearchField(GameList* parent) : QWidget{parent} {
+ auto* const key_release_eater = new KeyReleaseEater(parent, this);
+ layout_filter = new QHBoxLayout;
+ layout_filter->setContentsMargins(8, 8, 8, 8);
+ label_filter = new QLabel;
+ edit_filter = new QLineEdit;
+ edit_filter->clear();
+ edit_filter->installEventFilter(key_release_eater);
+ edit_filter->setClearButtonEnabled(true);
+ connect(edit_filter, &QLineEdit::textChanged, parent, &GameList::OnTextChanged);
+ label_filter_result = new QLabel;
+ button_filter_close = new QToolButton(this);
+ button_filter_close->setText(QStringLiteral("X"));
+ button_filter_close->setCursor(Qt::ArrowCursor);
+ button_filter_close->setStyleSheet(
+ QStringLiteral("QToolButton{ border: none; padding: 0px; color: "
+ "#000000; font-weight: bold; background: #F0F0F0; }"
+ "QToolButton:hover{ border: none; padding: 0px; color: "
+ "#EEEEEE; font-weight: bold; background: #E81123}"));
+ connect(button_filter_close, &QToolButton::clicked, parent, &GameList::OnFilterCloseClicked);
+ layout_filter->setSpacing(10);
+ layout_filter->addWidget(label_filter);
+ layout_filter->addWidget(edit_filter);
+ layout_filter->addWidget(label_filter_result);
+ layout_filter->addWidget(button_filter_close);
+ setLayout(layout_filter);
+ RetranslateUI();
+}
+
+void GameListSearchField::changeEvent(QEvent* event) {
+ if (event->type() == QEvent::LanguageChange) {
+ RetranslateUI();
+ }
+
+ QWidget::changeEvent(event);
+}
+
+void GameListSearchField::RetranslateUI() {
+ label_filter->setText(tr("Filter:"));
+ edit_filter->setPlaceholderText(tr("Enter pattern to filter"));
+}
diff --git a/src/yuzu/game/search_field.h b/src/yuzu/game/search_field.h
new file mode 100644
index 0000000000..18f97300f7
--- /dev/null
+++ b/src/yuzu/game/search_field.h
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QWidget>
+
+class GameList;
+class QHBoxLayout;
+class QTreeView;
+class QLabel;
+class QLineEdit;
+class QToolButton;
+
+class GameListSearchField : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit GameListSearchField(GameList* parent = nullptr);
+
+ QString filterText() const;
+ void setFilterResult(int visible_, int total_);
+
+ void clear();
+ void setFocus();
+
+private:
+ void changeEvent(QEvent*) override;
+ void RetranslateUI();
+
+ class KeyReleaseEater : public QObject {
+ public:
+ explicit KeyReleaseEater(GameList* gamelist_, QObject* parent = nullptr);
+
+ private:
+ GameList* gamelist = nullptr;
+ QString edit_filter_text_old;
+
+ protected:
+ // EventFilter in order to process systemkeys while editing the searchfield
+ bool eventFilter(QObject* obj, QEvent* event) override;
+ };
+ int visible;
+ int total;
+
+ QHBoxLayout* layout_filter = nullptr;
+ QTreeView* tree_view = nullptr;
+ QLabel* label_filter = nullptr;
+ QLineEdit* edit_filter = nullptr;
+ QLabel* label_filter_result = nullptr;
+ QToolButton* button_filter_close = nullptr;
+};
diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp
index e274aaf62d..c28ae31bee 100644
--- a/src/yuzu/multiplayer/chat_room.cpp
+++ b/src/yuzu/multiplayer/chat_room.cpp
@@ -20,7 +20,7 @@
#include "common/logging.h"
#include "network/announce_multiplayer_session.h"
#include "ui_chat_room.h"
-#include "yuzu/game/game_list_p.h"
+#include "qt_common/game_list/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 391ca56c05..31e5e20e52 100644
--- a/src/yuzu/multiplayer/client_room.cpp
+++ b/src/yuzu/multiplayer/client_room.cpp
@@ -14,7 +14,7 @@
#include "common/logging.h"
#include "network/announce_multiplayer_session.h"
#include "ui_client_room.h"
-#include "yuzu/game/game_list_p.h"
+#include "qt_common/game_list/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 2a5530e02c..ac6017e71f 100644
--- a/src/yuzu/multiplayer/host_room.cpp
+++ b/src/yuzu/multiplayer/host_room.cpp
@@ -20,7 +20,7 @@
#include "network/announce_multiplayer_session.h"
#include "qt_common/config/uisettings.h"
#include "ui_host_room.h"
-#include "yuzu/game/game_list_p.h"
+#include "qt_common/game_list/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 8e9ff6563f..d9e752896a 100644
--- a/src/yuzu/multiplayer/lobby.cpp
+++ b/src/yuzu/multiplayer/lobby.cpp
@@ -15,7 +15,7 @@
#include "network/network.h"
#include "qt_common/config/uisettings.h"
#include "ui_lobby.h"
-#include "yuzu/game/game_list_p.h"
+#include "qt_common/game_list/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/util/util.cpp b/src/yuzu/util/util.cpp
index e37d9d03e4..eb6583b358 100644
--- a/src/yuzu/util/util.cpp
+++ b/src/yuzu/util/util.cpp
@@ -4,14 +4,11 @@
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-#include <cmath>
#include <QPainter>
#include "applets/qt_profile_select.h"
-#include "common/logging.h"
#include "core/frontend/applets/profile_select.h"
#include "core/hle/service/acc/profile_manager.h"
-#include "frontend_common/data_manager.h"
#include "qt_common/qt_common.h"
#include "yuzu/util/util.h"
@@ -28,21 +25,6 @@ QFont GetMonospaceFont() {
return font;
}
-QString ReadableByteSize(qulonglong size) {
- return QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size));
-}
-
-QPixmap CreateCirclePixmapFromColor(const QColor& color) {
- QPixmap circle_pixmap(16, 16);
- circle_pixmap.fill(Qt::transparent);
- QPainter painter(&circle_pixmap);
- painter.setRenderHint(QPainter::Antialiasing);
- painter.setPen(color);
- painter.setBrush(color);
- painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0);
- return circle_pixmap;
-}
-
const std::optional<Common::UUID> GetProfileID() {
// if there's only a single profile, the user probably wants to use that... right?
const auto& profiles = QtCommon::system->GetProfileManager().FindExistingProfileUUIDs();
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h
index 9c3d5aa84a..2e4e325e22 100644
--- a/src/yuzu/util/util.h
+++ b/src/yuzu/util/util.h
@@ -13,16 +13,6 @@
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
[[nodiscard]] QFont GetMonospaceFont();
-/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
-[[nodiscard]] QString ReadableByteSize(qulonglong size);
-
-/**
- * Creates a circle pixmap from a specified color
- * @param color The color the pixmap shall have
- * @return QPixmap circle pixmap
- */
-[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
-
/**
* Prompt the user for a profile ID. If there is only one valid profile, returns that profile.
* @return The selected profile, or an std::nullopt if none were selected