mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-06-27 18:36:35 +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
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
const int card_width = cell_width - m_padding;
|
||||
|
||||
// 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 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// 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;
|
||||
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);
|
||||
|
|
|
|||
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
|
||||
|
||||
#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 SetupViews();
|
||||
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 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 {
|
||||
|
|
|
|||
|
|
@ -1,446 +0,0 @@
|
|||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QObject>
|
||||
#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"
|
||||
|
||||
enum class GameListItemType {
|
||||
Game = QStandardItem::UserType + 1,
|
||||
CustomDir = QStandardItem::UserType + 2,
|
||||
SdmcDir = QStandardItem::UserType + 3,
|
||||
UserNandDir = QStandardItem::UserType + 4,
|
||||
SysNandDir = QStandardItem::UserType + 5,
|
||||
AddDir = QStandardItem::UserType + 6,
|
||||
Favorites = QStandardItem::UserType + 7,
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(GameListItemType);
|
||||
|
||||
/**
|
||||
* Gets the default icon (for games without valid title metadata)
|
||||
* @param size The desired width and height of the default icon.
|
||||
* @return QPixmap default icon
|
||||
*/
|
||||
static QPixmap GetDefaultIcon(u32 size) {
|
||||
QPixmap icon(size, size);
|
||||
icon.fill(Qt::transparent);
|
||||
return icon;
|
||||
}
|
||||
|
||||
class GameListItem : public QStandardItem {
|
||||
|
||||
public:
|
||||
// used to access type from item index
|
||||
static constexpr int TypeRole = Qt::UserRole + 1;
|
||||
static constexpr int SortRole = Qt::UserRole + 2;
|
||||
GameListItem() = default;
|
||||
explicit GameListItem(const QString& string) : QStandardItem(string) {
|
||||
setData(string, SortRole);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialization of GameListItem for path values.
|
||||
* This class ensures that for every full path value it holds, a correct string representation
|
||||
* of just the filename (with no extension) will be displayed to the user.
|
||||
* If this class receives valid title metadata, it will also display game icons and titles.
|
||||
*/
|
||||
class GameListItemPath : public GameListItem {
|
||||
public:
|
||||
static constexpr int TitleRole = SortRole + 1;
|
||||
static constexpr int FullPathRole = SortRole + 2;
|
||||
static constexpr int ProgramIdRole = SortRole + 3;
|
||||
static constexpr int FileTypeRole = SortRole + 4;
|
||||
|
||||
GameListItemPath() = default;
|
||||
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
|
||||
const QString& game_name, const QString& game_type, u64 program_id,
|
||||
u64 play_time, const QString& patch_versions) {
|
||||
setData(type(), TypeRole);
|
||||
setData(game_path, FullPathRole);
|
||||
setData(game_name, TitleRole);
|
||||
setData(qulonglong(program_id), ProgramIdRole);
|
||||
setData(game_type, FileTypeRole);
|
||||
|
||||
const auto readable_play_time =
|
||||
play_time > 0 ? QObject::tr("Play Time: %1")
|
||||
.arg(QString::fromStdString(
|
||||
PlayTime::PlayTimeManager::GetReadablePlayTime(play_time)))
|
||||
: QObject::tr("Never Played");
|
||||
|
||||
const auto enabled_update = [patch_versions]() -> QString {
|
||||
const QStringList lines = patch_versions.split(QLatin1Char('\n'));
|
||||
const QRegularExpression regex{QStringLiteral(R"(^Update \(([0-9\.]+)\))")};
|
||||
for (const QString& line : std::as_const(lines)) {
|
||||
const auto match = regex.match(line);
|
||||
if (match.hasMatch() && match.hasCaptured(1))
|
||||
return QObject::tr("Version: %1").arg(match.captured(1));
|
||||
}
|
||||
return QObject::tr("Version: 1.0.0");
|
||||
}();
|
||||
|
||||
const auto tooltip = QStringLiteral("%1\n%2").arg(readable_play_time, enabled_update);
|
||||
|
||||
setData(tooltip, Qt::ToolTipRole);
|
||||
|
||||
const u32 size = UISettings::values.game_icon_size.GetValue();
|
||||
|
||||
QPixmap picture;
|
||||
if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) {
|
||||
picture = GetDefaultIcon(size);
|
||||
}
|
||||
picture = picture.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
setData(picture, Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
QVariant data(int role) const override {
|
||||
if (role == Qt::DisplayRole || role == SortRole) {
|
||||
std::string filename;
|
||||
Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename,
|
||||
nullptr);
|
||||
|
||||
const std::array<QString, 4> row_data{{
|
||||
QString::fromStdString(filename),
|
||||
data(FileTypeRole).toString(),
|
||||
QString::fromStdString(fmt::format("0x{:016X}", data(ProgramIdRole).toULongLong())),
|
||||
data(TitleRole).toString(),
|
||||
}};
|
||||
|
||||
const auto& row1 = row_data.at(UISettings::values.row_1_text_id.GetValue());
|
||||
// don't show row 2 on grid view
|
||||
switch (UISettings::values.game_list_mode.GetValue()) {
|
||||
|
||||
case Settings::GameListMode::TreeView: {
|
||||
const int row2_id = UISettings::values.row_2_text_id.GetValue();
|
||||
|
||||
if (role == SortRole) {
|
||||
return row1.toLower();
|
||||
}
|
||||
|
||||
// None
|
||||
if (row2_id == 4) {
|
||||
return row1;
|
||||
}
|
||||
|
||||
const auto& row2 = row_data.at(row2_id);
|
||||
|
||||
if (row1 == row2) {
|
||||
return row1;
|
||||
}
|
||||
|
||||
return QStringLiteral("%1\n %2").arg(row1, row2);
|
||||
}
|
||||
case Settings::GameListMode::GridView:
|
||||
return row1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GameListItem::data(role);
|
||||
}
|
||||
};
|
||||
|
||||
class GameListItemCompat : public GameListItem {
|
||||
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
|
||||
public:
|
||||
static constexpr int CompatNumberRole = SortRole;
|
||||
GameListItemCompat() = default;
|
||||
explicit GameListItemCompat(const QString& compatibility) {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
struct CompatStatus {
|
||||
QString color;
|
||||
const char* text;
|
||||
const char* tooltip;
|
||||
};
|
||||
// clang-format off
|
||||
const auto ingame_status =
|
||||
CompatStatus{QStringLiteral("#f2d624"), QT_TR_NOOP("Ingame"), QT_TR_NOOP("Game starts, but crashes or major glitches prevent it from being completed.")};
|
||||
static const std::map<QString, CompatStatus> status_data = {
|
||||
{QStringLiteral("0"), {QStringLiteral("#5c93ed"), QT_TR_NOOP("Perfect"), QT_TR_NOOP("Game can be played without issues.")}},
|
||||
{QStringLiteral("1"), {QStringLiteral("#47d35c"), QT_TR_NOOP("Playable"), QT_TR_NOOP("Game functions with minor graphical or audio glitches and is playable from start to finish.")}},
|
||||
{QStringLiteral("2"), ingame_status},
|
||||
{QStringLiteral("3"), ingame_status}, // Fallback for the removed "Okay" category
|
||||
{QStringLiteral("4"), {QStringLiteral("#FF0000"), QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("Game loads, but is unable to progress past the Start Screen.")}},
|
||||
{QStringLiteral("5"), {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}},
|
||||
{QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
auto iterator = status_data.find(compatibility);
|
||||
if (iterator == status_data.end()) {
|
||||
LOG_WARNING(Frontend, "Invalid compatibility number {}", compatibility.toStdString());
|
||||
return;
|
||||
}
|
||||
const CompatStatus& status = iterator->second;
|
||||
setData(compatibility, CompatNumberRole);
|
||||
setText(tr(status.text));
|
||||
setToolTip(tr(status.tooltip));
|
||||
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(CompatNumberRole).value<QString>() <
|
||||
other.data(CompatNumberRole).value<QString>();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialization of GameListItem for size values.
|
||||
* This class ensures that for every numerical size value it holds (in bytes), a correct
|
||||
* human-readable string representation will be displayed to the user.
|
||||
*/
|
||||
class GameListItemSize : public GameListItem {
|
||||
public:
|
||||
static constexpr int SizeRole = SortRole;
|
||||
|
||||
GameListItemSize() = default;
|
||||
explicit GameListItemSize(const qulonglong size_bytes) {
|
||||
setData(type(), TypeRole);
|
||||
setData(size_bytes, SizeRole);
|
||||
}
|
||||
|
||||
void setData(const QVariant& value, int role) override {
|
||||
// By specializing setData for SizeRole, we can ensure that the numerical and string
|
||||
// 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(value, SizeRole);
|
||||
} else {
|
||||
GameListItem::setData(value, role);
|
||||
}
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
/**
|
||||
* This operator is, in practice, only used by the TreeView sorting systems.
|
||||
* Override it so that it will correctly sort by numerical value instead of by string
|
||||
* representation.
|
||||
*/
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(SizeRole).toULongLong() < other.data(SizeRole).toULongLong();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GameListItem for Play Time values.
|
||||
* This object stores the play time of a game in seconds, and its readable
|
||||
* representation in minutes/hours
|
||||
*/
|
||||
class GameListItemPlayTime : public GameListItem {
|
||||
public:
|
||||
static constexpr int PlayTimeRole = SortRole;
|
||||
|
||||
GameListItemPlayTime() = default;
|
||||
explicit GameListItemPlayTime(const qulonglong time_seconds) {
|
||||
setData(time_seconds, PlayTimeRole);
|
||||
}
|
||||
|
||||
void setData(const QVariant& value, int role) override {
|
||||
qulonglong time_seconds = value.toULongLong();
|
||||
GameListItem::setData(
|
||||
QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)),
|
||||
Qt::DisplayRole);
|
||||
GameListItem::setData(value, PlayTimeRole);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
|
||||
}
|
||||
};
|
||||
|
||||
class GameListDir : public GameListItem {
|
||||
public:
|
||||
static constexpr int GameDirRole = Qt::UserRole + 2;
|
||||
|
||||
explicit GameListDir(UISettings::GameDir& directory,
|
||||
GameListItemType dir_type_ = GameListItemType::CustomDir)
|
||||
: dir_type{dir_type_} {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
UISettings::GameDir* game_dir = &directory;
|
||||
setData(QVariant(UISettings::values.game_dirs.indexOf(directory)), GameDirRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
switch (dir_type) {
|
||||
case GameListItemType::SdmcDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("sd_card"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::UserNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::SysNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("System Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::CustomDir: {
|
||||
const QString path = QString::fromStdString(game_dir->path);
|
||||
const QString icon_name =
|
||||
QFileInfo::exists(path) ? QStringLiteral("folder") : QStringLiteral("bad_folder");
|
||||
setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
|
||||
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(path, Qt::DisplayRole);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(dir_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to prevent automatic sorting between folders and the addDir button.
|
||||
*/
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
GameListItemType dir_type;
|
||||
};
|
||||
|
||||
class GameListAddDir : public GameListItem {
|
||||
public:
|
||||
explicit GameListAddDir() {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
|
||||
setData(QIcon::fromTheme(QStringLiteral("list-add"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::AddDir);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class GameListFavorites : public GameListItem {
|
||||
public:
|
||||
explicit GameListFavorites() {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
|
||||
setData(QIcon::fromTheme(QStringLiteral("star"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Favorites"), Qt::DisplayRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Favorites);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
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;
|
||||
};
|
||||
|
|
@ -1,496 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#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"
|
||||
|
||||
namespace {
|
||||
|
||||
QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
|
||||
const std::function<QString()>& generator) {
|
||||
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto path =
|
||||
Common::FS::PathToUTF8String(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) /
|
||||
"game_list" / fmt::format("{}.{}", filename, ext));
|
||||
|
||||
void(Common::FS::CreateParentDirs(path));
|
||||
|
||||
if (!Common::FS::Exists(path)) {
|
||||
const auto str = generator();
|
||||
|
||||
QFile file{QString::fromStdString(path)};
|
||||
if (file.open(QFile::WriteOnly)) {
|
||||
file.write(str.toUtf8());
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
QFile file{QString::fromStdString(path)};
|
||||
if (file.open(QFile::ReadOnly)) {
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
return generator();
|
||||
}
|
||||
|
||||
std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
|
||||
const std::string& filename, const std::string& ext,
|
||||
const std::function<std::pair<std::vector<u8>, std::string>()>& generator) {
|
||||
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto game_list_dir =
|
||||
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list";
|
||||
const auto jpeg_name = fmt::format("{}.jpeg", filename);
|
||||
const auto app_name = fmt::format("{}.appname.txt", filename);
|
||||
|
||||
const auto path1 = Common::FS::PathToUTF8String(game_list_dir / jpeg_name);
|
||||
const auto path2 = Common::FS::PathToUTF8String(game_list_dir / app_name);
|
||||
|
||||
void(Common::FS::CreateParentDirs(path1));
|
||||
|
||||
if (!Common::FS::Exists(path1) || !Common::FS::Exists(path2)) {
|
||||
const auto [icon, nacp] = generator();
|
||||
|
||||
QFile file1{QString::fromStdString(path1)};
|
||||
if (!file1.open(QFile::WriteOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (!file1.resize(icon.size())) {
|
||||
LOG_ERROR(Frontend, "Failed to resize cache file to necessary size.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (file1.write(reinterpret_cast<const char*>(icon.data()), icon.size()) !=
|
||||
s64(icon.size())) {
|
||||
LOG_ERROR(Frontend, "Failed to write data to cache file.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
QFile file2{QString::fromStdString(path2)};
|
||||
if (file2.open(QFile::WriteOnly)) {
|
||||
file2.write(nacp.data(), nacp.size());
|
||||
}
|
||||
|
||||
return std::make_pair(icon, nacp);
|
||||
}
|
||||
|
||||
QFile file1(QString::fromStdString(path1));
|
||||
QFile file2(QString::fromStdString(path2));
|
||||
|
||||
if (!file1.open(QFile::ReadOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (!file2.open(QFile::ReadOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
std::vector<u8> vec(file1.size());
|
||||
if (file1.read(reinterpret_cast<char*>(vec.data()), vec.size()) !=
|
||||
static_cast<s64>(vec.size())) {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto data = file2.readAll();
|
||||
return std::make_pair(vec, data.toStdString());
|
||||
}
|
||||
|
||||
void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
|
||||
std::vector<u8>& icon, std::string& name) {
|
||||
std::tie(icon, name) = GetGameListCachedObject(
|
||||
fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
|
||||
const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
|
||||
return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
|
||||
});
|
||||
}
|
||||
|
||||
bool HasSupportedFileExtension(const std::string& file_name) {
|
||||
const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
|
||||
return QtCommon::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool IsExtractedNCAMain(const std::string& file_name) {
|
||||
return QFileInfo(QString::fromStdString(file_name)).fileName() == QStringLiteral("main");
|
||||
}
|
||||
|
||||
QString FormatGameName(const std::string& physical_name) {
|
||||
const QString physical_name_as_qstring = QString::fromStdString(physical_name);
|
||||
const QFileInfo file_info(physical_name_as_qstring);
|
||||
|
||||
if (IsExtractedNCAMain(physical_name)) {
|
||||
return file_info.dir().path();
|
||||
}
|
||||
|
||||
return physical_name_as_qstring;
|
||||
}
|
||||
|
||||
QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
|
||||
Loader::AppLoader& loader, bool updatable = true) {
|
||||
QString out;
|
||||
FileSys::VirtualFile update_raw;
|
||||
loader.ReadUpdateRaw(update_raw);
|
||||
for (const auto& patch : patch_manager.GetPatches(update_raw)) {
|
||||
const bool is_update = patch.name == "Update";
|
||||
if (!updatable && is_update) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString type =
|
||||
QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
|
||||
|
||||
if (patch.version.empty()) {
|
||||
out.append(QStringLiteral("%1\n").arg(type));
|
||||
} else {
|
||||
auto ver = patch.version;
|
||||
|
||||
// Display container name for packed updates
|
||||
if (is_update && ver == "PACKED") {
|
||||
ver = Loader::GetFileTypeString(loader.GetFileType());
|
||||
}
|
||||
|
||||
out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver)));
|
||||
}
|
||||
}
|
||||
|
||||
out.chop(1);
|
||||
return out;
|
||||
}
|
||||
|
||||
QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
|
||||
const std::size_t size, const std::vector<u8>& icon,
|
||||
Loader::AppLoader& loader, u64 program_id,
|
||||
const CompatibilityList& compatibility_list,
|
||||
const PlayTime::PlayTimeManager& play_time_manager,
|
||||
const FileSys::PatchManager& patch) {
|
||||
auto const it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
|
||||
// The game list uses 99 as compatibility number for untested games
|
||||
QString compatibility =
|
||||
it != compatibility_list.end() ? it->second.first : QStringLiteral("99");
|
||||
|
||||
auto const file_type = loader.GetFileType();
|
||||
auto const file_type_string = QString::fromStdString(Loader::GetFileTypeString(file_type));
|
||||
|
||||
QString patch_versions = GetGameListCachedObject(
|
||||
fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
|
||||
return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
|
||||
});
|
||||
|
||||
u64 play_time = play_time_manager.GetPlayTime(program_id);
|
||||
return QList<QStandardItem*>{
|
||||
new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name),
|
||||
file_type_string, program_id, play_time, patch_versions),
|
||||
new GameListItem(file_type_string),
|
||||
new GameListItemSize(size),
|
||||
new GameListItemPlayTime(play_time),
|
||||
new GameListItem(patch_versions),
|
||||
new GameListItemCompat(compatibility),
|
||||
};
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
QVector<UISettings::GameDir>& game_dirs_,
|
||||
const CompatibilityList& compatibility_list_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_)
|
||||
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
|
||||
compatibility_list{compatibility_list_}, play_time_manager{play_time_manager_},
|
||||
system{system_} {
|
||||
// We want the game list to manage our lifetime.
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
GameListWorker::~GameListWorker() {
|
||||
this->disconnect();
|
||||
stop_requested.store(true);
|
||||
processing_completed.Wait();
|
||||
}
|
||||
|
||||
void GameListWorker::ProcessEvents(GameList* game_list) {
|
||||
while (true) {
|
||||
std::function<void(GameList*)> func;
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// If we can't pop a function, return.
|
||||
if (queued_events.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pop a function.
|
||||
func = std::move(queued_events.back());
|
||||
queued_events.pop_back();
|
||||
}
|
||||
|
||||
// Run the function.
|
||||
func(game_list);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void GameListWorker::RecordEvent(F&& func) {
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// Add the function into the front of the queue.
|
||||
queued_events.emplace_front(std::move(func));
|
||||
}
|
||||
|
||||
// Data now available.
|
||||
emit DataAvailable();
|
||||
}
|
||||
|
||||
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
|
||||
using namespace FileSys;
|
||||
|
||||
const auto& cache = system.GetContentProviderUnion();
|
||||
|
||||
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
|
||||
ContentRecordType::Program);
|
||||
|
||||
if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
|
||||
}
|
||||
|
||||
for (const auto& [slot, game] : installed_games) {
|
||||
if (slot == ContentProviderUnionSlot::FrontendManual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
|
||||
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(system, file);
|
||||
if (!loader) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u8> icon;
|
||||
std::string name;
|
||||
u64 program_id = 0;
|
||||
const auto result = loader->ReadProgramId(program_id);
|
||||
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
LOG_INFO(Frontend, "PatchManager initiated for id {:X}", program_id);
|
||||
const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
|
||||
if (control != nullptr) {
|
||||
GetMetadataFromControlNCA(patch, *control, icon, name);
|
||||
}
|
||||
|
||||
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); });
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir) {
|
||||
const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
|
||||
if (stop_requested) {
|
||||
// Breaks the callback loop.
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto physical_name = Common::FS::PathToUTF8String(path);
|
||||
const auto is_dir = Common::FS::IsDir(path);
|
||||
|
||||
if (!is_dir &&
|
||||
(HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
|
||||
const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
|
||||
if (!file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(system, file);
|
||||
if (!loader) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
u64 program_id = 0;
|
||||
const auto res2 = loader->ReadProgramId(program_id);
|
||||
|
||||
if (target == ScanTarget::FillManualContentProvider) {
|
||||
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
|
||||
provider->AddEntry(FileSys::TitleType::Application,
|
||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||
program_id, file);
|
||||
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||
(file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSP)) {
|
||||
void(provider->AddEntriesFromContainer(file));
|
||||
}
|
||||
} else {
|
||||
std::vector<u64> program_ids;
|
||||
loader->ReadProgramIds(program_ids);
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
for (const auto id : program_ids) {
|
||||
// dravee suggested this, only viable way to
|
||||
// not show sub-games in qlaunch for now.
|
||||
if ((id & 0xFFF) != 0) {
|
||||
continue;
|
||||
}
|
||||
loader = Loader::GetLoader(system, file, id);
|
||||
if (!loader) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u8> icon;
|
||||
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
|
||||
|
||||
std::string name = " ";
|
||||
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
|
||||
|
||||
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
} else {
|
||||
std::vector<u8> icon;
|
||||
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
|
||||
|
||||
std::string name = " ";
|
||||
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
|
||||
|
||||
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
} else if (is_dir) {
|
||||
watch_list.append(QString::fromStdString(physical_name));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (deep_scan) {
|
||||
Common::FS::IterateDirEntriesRecursively(dir_path, callback,
|
||||
Common::FS::DirEntryFilter::All);
|
||||
} else {
|
||||
Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::run() {
|
||||
watch_list.clear();
|
||||
provider->ClearAllEntries();
|
||||
|
||||
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
|
||||
};
|
||||
|
||||
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||
if (stop_requested) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (game_dir.path == std::string("SDMC")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == std::string("UserNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == std::string("SysNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else {
|
||||
const QString qpath = QString::fromStdString(game_dir.path);
|
||||
if (QDir(qpath).exists()) {
|
||||
watch_list.append(qpath);
|
||||
}
|
||||
auto* const game_list_dir = new GameListDir(game_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
|
||||
game_list_dir);
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
|
||||
game_list_dir);
|
||||
}
|
||||
}
|
||||
|
||||
RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); });
|
||||
processing_completed.Set();
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
|
||||
#include "common/thread.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "frontend_common/play_time_manager.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
class GameList;
|
||||
class GameListDir;
|
||||
class QStandardItem;
|
||||
|
||||
namespace FileSys {
|
||||
class NCA;
|
||||
class VfsFilesystem;
|
||||
} // namespace FileSys
|
||||
|
||||
/**
|
||||
* Asynchronous worker object for populating the game list.
|
||||
* Communicates with other threads through Qt's signal/slot system.
|
||||
*/
|
||||
class GameListWorker : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
QVector<UISettings::GameDir>& game_dirs_,
|
||||
const CompatibilityList& compatibility_list_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_);
|
||||
~GameListWorker() override;
|
||||
|
||||
/// Starts the processing of directory tree information.
|
||||
void run() override;
|
||||
|
||||
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.
|
||||
*/
|
||||
void ProcessEvents(GameList* game_list);
|
||||
|
||||
signals:
|
||||
void DataAvailable();
|
||||
|
||||
private:
|
||||
template <typename F>
|
||||
void RecordEvent(F&& func);
|
||||
|
||||
private:
|
||||
void AddTitlesToGameList(GameListDir* parent_dir);
|
||||
|
||||
enum class ScanTarget {
|
||||
FillManualContentProvider,
|
||||
PopulateGameList,
|
||||
};
|
||||
|
||||
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir);
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
QVector<UISettings::GameDir>& game_dirs;
|
||||
const CompatibilityList& compatibility_list;
|
||||
const PlayTime::PlayTimeManager& play_time_manager;
|
||||
|
||||
QStringList watch_list;
|
||||
|
||||
std::mutex lock;
|
||||
std::condition_variable cv;
|
||||
std::deque<std::function<void(GameList*)>> queued_events;
|
||||
std::atomic_bool stop_requested = false;
|
||||
Common::Event processing_completed;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
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 "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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue