mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-06-04 22:27:09 +02:00
[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>
This commit is contained in:
parent
27189f39d2
commit
cc8451f764
25 changed files with 1307 additions and 1045 deletions
|
|
@ -31,6 +31,10 @@ add_library(qt_common STATIC
|
||||||
abstract/frontend.h abstract/frontend.cpp
|
abstract/frontend.h abstract/frontend.cpp
|
||||||
abstract/progress.h abstract/progress.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_string_lookup.h
|
||||||
qt_compat.h
|
qt_compat.h
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,13 @@
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging.h"
|
#include "common/logging.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "frontend_common/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
#include "qt_common/config/uisettings.h"
|
#include "qt_common/config/uisettings.h"
|
||||||
#include "yuzu/util/util.h"
|
#include "qt_common/qt_common.h"
|
||||||
|
|
||||||
enum class GameListItemType {
|
enum class GameListItemType {
|
||||||
Game = QStandardItem::UserType + 1,
|
Game = QStandardItem::UserType + 1,
|
||||||
|
|
@ -204,7 +203,7 @@ public:
|
||||||
setData(compatibility, CompatNumberRole);
|
setData(compatibility, CompatNumberRole);
|
||||||
setText(tr(status.text));
|
setText(tr(status.text));
|
||||||
setToolTip(tr(status.tooltip));
|
setToolTip(tr(status.tooltip));
|
||||||
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
setData(QtCommon::CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
int type() const override {
|
int type() const override {
|
||||||
|
|
@ -237,7 +236,7 @@ public:
|
||||||
// representations of the data are always accurate and in the correct format.
|
// representations of the data are always accurate and in the correct format.
|
||||||
if (role == SizeRole) {
|
if (role == SizeRole) {
|
||||||
qulonglong size_bytes = value.toULongLong();
|
qulonglong size_bytes = value.toULongLong();
|
||||||
GameListItem::setData(ReadableByteSize(size_bytes), Qt::DisplayRole);
|
GameListItem::setData(QtCommon::ReadableByteSize(size_bytes), Qt::DisplayRole);
|
||||||
GameListItem::setData(value, SizeRole);
|
GameListItem::setData(value, SizeRole);
|
||||||
} else {
|
} else {
|
||||||
GameListItem::setData(value, role);
|
GameListItem::setData(value, role);
|
||||||
|
|
@ -398,49 +397,3 @@ public:
|
||||||
return false;
|
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;
|
|
||||||
};
|
|
||||||
303
src/qt_common/game_list/model.cpp
Normal file
303
src/qt_common/game_list/model.cpp
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
98
src/qt_common/game_list/model.h
Normal file
98
src/qt_common/game_list/model.h
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -33,9 +33,10 @@
|
||||||
#include "qt_common/qt_common.h"
|
#include "qt_common/qt_common.h"
|
||||||
|
|
||||||
#include "yuzu/compatibility_list.h"
|
#include "yuzu/compatibility_list.h"
|
||||||
#include "yuzu/game/game_list.h"
|
#include "qt_common/game_list/game_list_p.h"
|
||||||
#include "yuzu/game/game_list_p.h"
|
|
||||||
#include "yuzu/game/game_list_worker.h"
|
#include "qt_common/game_list/worker.h"
|
||||||
|
#include "qt_common/game_list/model.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
@ -250,9 +251,9 @@ GameListWorker::~GameListWorker() {
|
||||||
processing_completed.Wait();
|
processing_completed.Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListWorker::ProcessEvents(GameList* game_list) {
|
void GameListWorker::ProcessEvents(GameListModel* model) {
|
||||||
while (true) {
|
while (true) {
|
||||||
std::function<void(GameList*)> func;
|
std::function<void(GameListModel*)> func;
|
||||||
{
|
{
|
||||||
// Lock queue to protect concurrent modification.
|
// Lock queue to protect concurrent modification.
|
||||||
std::scoped_lock lk(lock);
|
std::scoped_lock lk(lock);
|
||||||
|
|
@ -268,7 +269,7 @@ void GameListWorker::ProcessEvents(GameList* game_list) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the function.
|
// 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,
|
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
|
||||||
program_id, compatibility_list, play_time_manager, patch);
|
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);
|
id, compatibility_list, play_time_manager, patch);
|
||||||
|
|
||||||
RecordEvent(
|
RecordEvent(
|
||||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
[=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::vector<u8> icon;
|
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);
|
program_id, compatibility_list, play_time_manager, patch);
|
||||||
|
|
||||||
RecordEvent(
|
RecordEvent(
|
||||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
[=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_dir) {
|
} else if (is_dir) {
|
||||||
|
|
@ -457,7 +458,7 @@ void GameListWorker::run() {
|
||||||
provider->ClearAllEntries();
|
provider->ClearAllEntries();
|
||||||
|
|
||||||
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
|
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) {
|
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();
|
processing_completed.Set();
|
||||||
}
|
}
|
||||||
|
|
@ -26,8 +26,8 @@ namespace Core {
|
||||||
class System;
|
class System;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GameList;
|
|
||||||
class GameListDir;
|
class GameListDir;
|
||||||
|
class GameListModel;
|
||||||
class QStandardItem;
|
class QStandardItem;
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
|
|
@ -58,11 +58,11 @@ public:
|
||||||
/**
|
/**
|
||||||
* Synchronously processes any events queued by the worker.
|
* Synchronously processes any events queued by the worker.
|
||||||
*
|
*
|
||||||
* AddDirEntry is called on the game list for every discovered directory.
|
* AddDirEntry is called on the model for every discovered directory.
|
||||||
* AddEntry is called on the game list for every discovered program.
|
* AddEntry is called on the model for every discovered program.
|
||||||
* DonePopulating is called on the game list when processing completes.
|
* DonePopulating is called on the model when processing completes.
|
||||||
*/
|
*/
|
||||||
void ProcessEvents(GameList* game_list);
|
void ProcessEvents(GameListModel* model);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void DataAvailable();
|
void DataAvailable();
|
||||||
|
|
@ -92,7 +92,7 @@ private:
|
||||||
|
|
||||||
std::mutex lock;
|
std::mutex lock;
|
||||||
std::condition_variable cv;
|
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;
|
std::atomic_bool stop_requested = false;
|
||||||
Common::Event processing_completed;
|
Common::Event processing_completed;
|
||||||
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include "common/memory_detect.h"
|
#include "common/memory_detect.h"
|
||||||
#include "core/hle/service/filesystem/filesystem.h"
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
#include "frontend_common/data_manager.h"
|
||||||
#include "hid_core/hid_core.h"
|
#include "hid_core/hid_core.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
#include "qt_common.h"
|
#include "qt_common.h"
|
||||||
|
|
@ -27,6 +28,7 @@
|
||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <JlCompress.h>
|
#include <JlCompress.h>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
#if !defined(WIN32) && !defined(__APPLE__)
|
#if !defined(WIN32) && !defined(__APPLE__)
|
||||||
#include <qpa/qplatformnativeinterface.h>
|
#include <qpa/qplatformnativeinterface.h>
|
||||||
|
|
@ -307,4 +309,19 @@ void SetupHID() {
|
||||||
system->HIDCore().ReloadInputDevices();
|
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
|
} // namespace QtCommon
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,20 @@ void SetupHID();
|
||||||
const QString tr(const char* str);
|
const QString tr(const char* str);
|
||||||
const QString tr(const std::string& 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();
|
std::filesystem::path GetEdenCommand();
|
||||||
} // namespace QtCommon
|
} // namespace QtCommon
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -156,13 +156,12 @@ add_executable(yuzu
|
||||||
debugger/controller.cpp
|
debugger/controller.cpp
|
||||||
debugger/controller.h
|
debugger/controller.h
|
||||||
|
|
||||||
game/game_list.cpp
|
game/game_list.cpp game/game_list.h
|
||||||
game/game_list.h
|
game/game_grid.cpp game/game_grid.h
|
||||||
game/game_list_p.h
|
|
||||||
game/game_list_worker.cpp
|
game/game_tree.h game/game_tree.cpp
|
||||||
game/game_list_worker.h
|
game/game_card.h game/game_card.cpp
|
||||||
game/game_card.h
|
game/search_field.h game/search_field.cpp
|
||||||
game/game_card.cpp
|
|
||||||
|
|
||||||
hotkeys.cpp
|
hotkeys.cpp
|
||||||
hotkeys.h
|
hotkeys.h
|
||||||
|
|
@ -219,18 +218,16 @@ add_executable(yuzu
|
||||||
util/util.h
|
util/util.h
|
||||||
compatdb.cpp
|
compatdb.cpp
|
||||||
compatdb.h
|
compatdb.h
|
||||||
user_data_migration.cpp
|
|
||||||
user_data_migration.h
|
user_data_migration.h user_data_migration.cpp
|
||||||
|
|
||||||
yuzu.qrc
|
yuzu.qrc
|
||||||
yuzu.rc
|
yuzu.rc
|
||||||
migration_dialog.h migration_dialog.cpp
|
migration_dialog.h migration_dialog.cpp
|
||||||
migration_worker.h
|
migration_worker.h migration_worker.cpp
|
||||||
migration_worker.cpp
|
libqt_common.h libqt_common.cpp
|
||||||
|
|
||||||
deps_dialog.cpp
|
deps_dialog.cpp deps_dialog.h deps_dialog.ui
|
||||||
deps_dialog.h
|
|
||||||
deps_dialog.ui
|
|
||||||
|
|
||||||
data_dialog.h data_dialog.cpp data_dialog.ui
|
data_dialog.h data_dialog.cpp data_dialog.ui
|
||||||
data_widget.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
|
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
|
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)
|
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui)
|
||||||
|
|
||||||
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
|
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
#include "frontend_common/config.h"
|
#include "frontend_common/config.h"
|
||||||
#include "qt_common/config/uisettings.h"
|
#include "qt_common/config/uisettings.h"
|
||||||
|
#include "qt_common/qt_common.h"
|
||||||
#include "qt_common/util/vk.h"
|
#include "qt_common/util/vk.h"
|
||||||
#include "ui_configure_per_game.h"
|
#include "ui_configure_per_game.h"
|
||||||
#include "yuzu/configuration/configuration_shared.h"
|
#include "yuzu/configuration/configuration_shared.h"
|
||||||
|
|
@ -205,6 +206,6 @@ void ConfigurePerGame::LoadConfiguration() {
|
||||||
ui->display_format->setText(
|
ui->display_format->setText(
|
||||||
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())));
|
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())));
|
||||||
|
|
||||||
const auto valueText = ReadableByteSize(file->GetSize());
|
const auto valueText = QtCommon::ReadableByteSize(file->GetSize());
|
||||||
ui->display_size->setText(valueText);
|
ui->display_size->setText(valueText);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,34 +18,27 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
painter->save();
|
painter->save();
|
||||||
painter->setRenderHint(QPainter::Antialiasing);
|
painter->setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
// Padding, dimensions, alignment...
|
constexpr int cardMargin = 8;
|
||||||
|
constexpr int cardCornerRadius = 10;
|
||||||
|
|
||||||
const int column = index.row() % m_columns;
|
const int column = index.row() % m_columns;
|
||||||
const int cell_width = option.rect.width();
|
const int cell_width = option.rect.width();
|
||||||
const int fixed_card_width = cell_width - m_padding;
|
const int 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,
|
const int row_width = m_columns * cell_width;
|
||||||
// while maintaining an even gap between each card.
|
const int total_gap = row_width - cardMargin * 2 - m_columns * card_width;
|
||||||
// I just smashed random keys into my keyboard until something worked.
|
const int gap = (m_columns > 1) ? (total_gap / (m_columns - 1)) : 0;
|
||||||
// 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 auto relative_x = margins + (column * (fixed_card_width + gap));
|
const int card_left = option.rect.left() - column * cell_width + cardMargin + column * (card_width + gap) + 4;
|
||||||
const auto x_pos = option.rect.left() - (column * cell_width) + static_cast<int>(relative_x);
|
const QRect cardRect(card_left, option.rect.top() + 4, card_width - 8,
|
||||||
|
option.rect.height() - cardMargin);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// colors
|
|
||||||
QPalette palette = option.palette;
|
QPalette palette = option.palette;
|
||||||
QColor backgroundColor = palette.window().color();
|
QColor backgroundColor = palette.window().color();
|
||||||
QColor borderColor = palette.dark().color();
|
QColor borderColor = palette.dark().color();
|
||||||
QColor textColor = palette.text().color();
|
QColor textColor = palette.text().color();
|
||||||
|
|
||||||
// if it's selected add a blue background
|
// highlight blue on select
|
||||||
if (option.state & QStyle::State_Selected) {
|
if (option.state & QStyle::State_Selected) {
|
||||||
backgroundColor = palette.highlight().color();
|
backgroundColor = palette.highlight().color();
|
||||||
borderColor = palette.highlight().color().lighter(150);
|
borderColor = palette.highlight().color().lighter(150);
|
||||||
|
|
@ -54,63 +47,45 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
backgroundColor = backgroundColor.lighter(120);
|
backgroundColor = backgroundColor.lighter(120);
|
||||||
}
|
}
|
||||||
|
|
||||||
// bg
|
|
||||||
painter->setBrush(backgroundColor);
|
painter->setBrush(backgroundColor);
|
||||||
painter->setPen(QPen(borderColor, 1));
|
painter->setPen(QPen(borderColor, 1));
|
||||||
painter->drawRoundedRect(cardRect, 10, 10);
|
painter->drawRoundedRect(cardRect, cardCornerRadius, cardCornerRadius);
|
||||||
|
|
||||||
// icon
|
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
|
||||||
int _iconsize = UISettings::values.game_icon_size.GetValue();
|
QPixmap icon_pixmap = index.data(Qt::DecorationRole).value<QPixmap>();
|
||||||
QSize iconSize(_iconsize, _iconsize);
|
|
||||||
QPixmap iconPixmap = index.data(Qt::DecorationRole).value<QPixmap>();
|
|
||||||
|
|
||||||
QRect iconRect;
|
QRect iconRect;
|
||||||
if (!iconPixmap.isNull()) {
|
if (!icon_pixmap.isNull()) {
|
||||||
QSize scaledSize = iconPixmap.size();
|
QSize scaled = icon_pixmap.size();
|
||||||
scaledSize.scale(iconSize, Qt::KeepAspectRatio);
|
scaled.scale(icon_size, icon_size, Qt::KeepAspectRatio);
|
||||||
|
|
||||||
int x = cardRect.left() + (cardRect.width() - scaledSize.width()) / 2;
|
iconRect = {cardRect.left() + (cardRect.width() - scaled.width()) / 2,
|
||||||
int y = cardRect.top() + margins;
|
cardRect.top() + cardMargin, scaled.width(), scaled.height()};
|
||||||
|
|
||||||
iconRect = QRect(x, y, scaledSize.width(), scaledSize.height());
|
|
||||||
|
|
||||||
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
|
||||||
// Put this in a separate thing on the painter stack to prevent clipping the text.
|
|
||||||
painter->save();
|
painter->save();
|
||||||
|
QPainterPath clip_path;
|
||||||
// round image edges
|
clip_path.addRoundedRect(iconRect, cardCornerRadius, cardCornerRadius);
|
||||||
QPainterPath path;
|
painter->setClipPath(clip_path);
|
||||||
path.addRoundedRect(iconRect, 10, 10);
|
painter->drawPixmap(iconRect, icon_pixmap);
|
||||||
painter->setClipPath(path);
|
|
||||||
|
|
||||||
painter->drawPixmap(iconRect, iconPixmap);
|
|
||||||
|
|
||||||
painter->restore();
|
painter->restore();
|
||||||
} else {
|
} else {
|
||||||
// if there is no icon just draw a blank rect
|
iconRect = {cardRect.left() + cardMargin, cardRect.top() + cardMargin,
|
||||||
iconRect = QRect(cardRect.left() + margins, cardRect.top() + margins, _iconsize, _iconsize);
|
static_cast<int>(icon_size), static_cast<int>(icon_size)};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UISettings::values.show_game_name.GetValue()) {
|
if (UISettings::values.show_game_name.GetValue()) {
|
||||||
// padding + text
|
|
||||||
QRect textRect = cardRect;
|
QRect textRect = cardRect;
|
||||||
textRect.setTop(iconRect.bottom() + margins);
|
textRect.setTop(iconRect.bottom() + cardMargin);
|
||||||
textRect.adjust(margins, 0, -margins, -margins);
|
textRect.adjust(cardMargin, 0, -cardMargin, -cardMargin);
|
||||||
|
|
||||||
// We are already crammed on space, ignore the row 2
|
QString title = index.data(Qt::DisplayRole).toString().split(QLatin1Char('\n')).first();
|
||||||
QString title = index.data(Qt::DisplayRole).toString();
|
|
||||||
title = title.split(QLatin1Char('\n')).first();
|
|
||||||
|
|
||||||
// now draw text
|
|
||||||
painter->setPen(textColor);
|
painter->setPen(textColor);
|
||||||
QFont font = option.font;
|
QFont font = option.font;
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
|
font.setPixelSize(std::max(11.0, std::sqrt(static_cast<double>(icon_size))));
|
||||||
// TODO(crueter): fix this abysmal scaling
|
|
||||||
font.setPixelSize(1.5 + std::max(10.0, std::sqrt(_iconsize)));
|
|
||||||
|
|
||||||
// TODO(crueter): elide mode
|
|
||||||
painter->setFont(font);
|
painter->setFont(font);
|
||||||
|
|
||||||
painter->drawText(textRect, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, title);
|
painter->drawText(textRect, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, title);
|
||||||
|
|
|
||||||
118
src/yuzu/game/game_grid.cpp
Normal file
118
src/yuzu/game/game_grid.cpp
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/yuzu/game/game_grid.h
Normal file
26
src/yuzu/game/game_grid.h
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,14 +6,12 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QFileSystemWatcher>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTreeView>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
@ -28,21 +26,22 @@
|
||||||
|
|
||||||
class QVariantAnimation;
|
class QVariantAnimation;
|
||||||
|
|
||||||
class QListView;
|
|
||||||
|
|
||||||
class GameCard;
|
class GameCard;
|
||||||
namespace Core {
|
class GameListModel;
|
||||||
class System;
|
class GameTree;
|
||||||
}
|
class GameGrid;
|
||||||
|
class GameListSearchField;
|
||||||
class ControllerNavigation;
|
class ControllerNavigation;
|
||||||
class GameListWorker;
|
class GameListWorker;
|
||||||
class GameListSearchField;
|
|
||||||
class GameListDir;
|
class GameListDir;
|
||||||
class MainWindow;
|
class MainWindow;
|
||||||
enum class AmLaunchType;
|
enum class AmLaunchType;
|
||||||
enum class StartGameType;
|
enum class StartGameType;
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
class ManualContentProvider;
|
class ManualContentProvider;
|
||||||
class VfsFilesystem;
|
class VfsFilesystem;
|
||||||
|
|
@ -62,16 +61,6 @@ class GameList : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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_,
|
explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||||
FileSys::ManualContentProvider* provider_,
|
FileSys::ManualContentProvider* provider_,
|
||||||
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
|
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
|
||||||
|
|
@ -95,13 +84,11 @@ public:
|
||||||
/// Disables events from the emulated controller
|
/// Disables events from the emulated controller
|
||||||
void UnloadController();
|
void UnloadController();
|
||||||
|
|
||||||
bool IsTreeMode();
|
|
||||||
void ResetViewMode();
|
void ResetViewMode();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void RefreshGameDirectory();
|
void RefreshGameDirectory();
|
||||||
void RefreshExternalContent();
|
void RefreshExternalContent();
|
||||||
void ResetExternalWatcher();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void BootGame(const QString& game_path, StartGameType type);
|
void BootGame(const QString& game_path, StartGameType type);
|
||||||
|
|
@ -130,27 +117,20 @@ signals:
|
||||||
void SaveConfig();
|
void SaveConfig();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void OnItemExpanded(const QModelIndex& item);
|
|
||||||
void OnTextChanged(const QString& new_text);
|
void OnTextChanged(const QString& new_text);
|
||||||
void OnFilterCloseClicked();
|
void OnFilterCloseClicked();
|
||||||
void OnUpdateThemedIcons();
|
void OnUpdateThemedIcons();
|
||||||
|
void OnPopulatingCompleted(const QStringList& watch_list);
|
||||||
void UpdateIconSize();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class GameListWorker;
|
void SetupViews();
|
||||||
void WorkerEvent();
|
void SetupScrollAnimation();
|
||||||
|
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||||
|
void changeEvent(QEvent*) override;
|
||||||
|
|
||||||
void AddDirEntry(GameListDir* entry_items);
|
|
||||||
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
|
|
||||||
void DonePopulating(const QStringList& watch_list);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ValidateEntry(const QModelIndex& item);
|
void ValidateEntry(const QModelIndex& item);
|
||||||
|
|
||||||
void ToggleFavorite(u64 program_id);
|
void ToggleFavorite(u64 program_id);
|
||||||
void AddFavorite(u64 program_id);
|
|
||||||
void RemoveFavorite(u64 program_id);
|
|
||||||
|
|
||||||
void PopupContextMenu(const QPoint& menu_location);
|
void PopupContextMenu(const QPoint& menu_location);
|
||||||
void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
|
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 AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||||
void AddFavoritesPopup(QMenu& context_menu);
|
void AddFavoritesPopup(QMenu& context_menu);
|
||||||
|
|
||||||
void changeEvent(QEvent*) override;
|
|
||||||
void RetranslateUI();
|
void RetranslateUI();
|
||||||
|
|
||||||
|
friend class GameListSearchField;
|
||||||
|
|
||||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||||
FileSys::ManualContentProvider* provider;
|
FileSys::ManualContentProvider* provider;
|
||||||
GameListSearchField* search_field;
|
GameListSearchField* search_field = nullptr;
|
||||||
MainWindow* main_window = nullptr;
|
MainWindow* main_window = nullptr;
|
||||||
QVBoxLayout* layout = nullptr;
|
QVBoxLayout* layout = nullptr;
|
||||||
|
|
||||||
QTreeView* tree_view = nullptr;
|
GameTree* tree_view = nullptr;
|
||||||
QListView* list_view = nullptr;
|
GameGrid* grid_view = nullptr;
|
||||||
GameCard* m_gameCard = 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;
|
ControllerNavigation* controller_navigation = nullptr;
|
||||||
CompatibilityList compatibility_list;
|
|
||||||
|
|
||||||
QVariantAnimation* vertical_scroll = nullptr;
|
QVariantAnimation* vertical_scroll = nullptr;
|
||||||
QVariantAnimation* horizontal_scroll = nullptr;
|
QVariantAnimation* horizontal_scroll = nullptr;
|
||||||
int vertical_scroll_target = 0;
|
int vertical_scroll_target = 0;
|
||||||
int horizontal_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;
|
const PlayTime::PlayTimeManager& play_time_manager;
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
|
|
||||||
bool m_isTreeMode = true;
|
bool m_isTreeMode = true;
|
||||||
QAbstractItemView* m_currentView = tree_view;
|
QAbstractItemView* m_currentView = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GameListPlaceholder : public QWidget {
|
class GameListPlaceholder : public QWidget {
|
||||||
|
|
|
||||||
173
src/yuzu/game/game_tree.cpp
Normal file
173
src/yuzu/game/game_tree.cpp
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
35
src/yuzu/game/game_tree.h
Normal file
35
src/yuzu/game/game_tree.h
Normal file
|
|
@ -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);
|
||||||
|
};
|
||||||
124
src/yuzu/game/search_field.cpp
Normal file
124
src/yuzu/game/search_field.cpp
Normal file
|
|
@ -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"));
|
||||||
|
}
|
||||||
52
src/yuzu/game/search_field.h
Normal file
52
src/yuzu/game/search_field.h
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
#include "common/logging.h"
|
#include "common/logging.h"
|
||||||
#include "network/announce_multiplayer_session.h"
|
#include "network/announce_multiplayer_session.h"
|
||||||
#include "ui_chat_room.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/chat_room.h"
|
||||||
#include "yuzu/multiplayer/message.h"
|
#include "yuzu/multiplayer/message.h"
|
||||||
#ifdef ENABLE_WEB_SERVICE
|
#ifdef ENABLE_WEB_SERVICE
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
#include "common/logging.h"
|
#include "common/logging.h"
|
||||||
#include "network/announce_multiplayer_session.h"
|
#include "network/announce_multiplayer_session.h"
|
||||||
#include "ui_client_room.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/client_room.h"
|
||||||
#include "yuzu/multiplayer/message.h"
|
#include "yuzu/multiplayer/message.h"
|
||||||
#include "yuzu/multiplayer/moderation_dialog.h"
|
#include "yuzu/multiplayer/moderation_dialog.h"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
#include "network/announce_multiplayer_session.h"
|
#include "network/announce_multiplayer_session.h"
|
||||||
#include "qt_common/config/uisettings.h"
|
#include "qt_common/config/uisettings.h"
|
||||||
#include "ui_host_room.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/main_window.h"
|
||||||
#include "yuzu/multiplayer/host_room.h"
|
#include "yuzu/multiplayer/host_room.h"
|
||||||
#include "yuzu/multiplayer/message.h"
|
#include "yuzu/multiplayer/message.h"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
#include "qt_common/config/uisettings.h"
|
#include "qt_common/config/uisettings.h"
|
||||||
#include "ui_lobby.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/main_window.h"
|
||||||
#include "yuzu/multiplayer/client_room.h"
|
#include "yuzu/multiplayer/client_room.h"
|
||||||
#include "yuzu/multiplayer/lobby.h"
|
#include "yuzu/multiplayer/lobby.h"
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,11 @@
|
||||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|
||||||
#include "applets/qt_profile_select.h"
|
#include "applets/qt_profile_select.h"
|
||||||
#include "common/logging.h"
|
|
||||||
#include "core/frontend/applets/profile_select.h"
|
#include "core/frontend/applets/profile_select.h"
|
||||||
#include "core/hle/service/acc/profile_manager.h"
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
#include "frontend_common/data_manager.h"
|
|
||||||
#include "qt_common/qt_common.h"
|
#include "qt_common/qt_common.h"
|
||||||
#include "yuzu/util/util.h"
|
#include "yuzu/util/util.h"
|
||||||
|
|
||||||
|
|
@ -28,21 +25,6 @@ QFont GetMonospaceFont() {
|
||||||
return font;
|
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() {
|
const std::optional<Common::UUID> GetProfileID() {
|
||||||
// if there's only a single profile, the user probably wants to use that... right?
|
// if there's only a single profile, the user probably wants to use that... right?
|
||||||
const auto& profiles = QtCommon::system->GetProfileManager().FindExistingProfileUUIDs();
|
const auto& profiles = QtCommon::system->GetProfileManager().FindExistingProfileUUIDs();
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,6 @@
|
||||||
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
|
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
|
||||||
[[nodiscard]] QFont GetMonospaceFont();
|
[[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.
|
* 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
|
* @return The selected profile, or an std::nullopt if none were selected
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue