yuzu: Add support for multiple game directories

Ported from https://github.com/citra-emu/citra/pull/3617.
This commit is contained in:
fearlessTobi 2019-05-01 23:21:04 +02:00 committed by FearlessTobi
parent 7fc5af3622
commit 2d8eba5baf
12 changed files with 664 additions and 193 deletions

View file

@ -517,10 +517,35 @@ void Config::ReadPathValues() {
UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString(); UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString();
UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString(); UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString();
UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString();
UISettings::values.game_directory_path = UISettings::values.game_dir_deprecated =
ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString();
UISettings::values.game_directory_deepscan = UISettings::values.game_dir_deprecated_deepscan =
ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool();
int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs"));
for (int i = 0; i < gamedirs_size; ++i) {
qt_config->setArrayIndex(i);
UISettings::GameDir game_dir;
game_dir.path = ReadSetting(QStringLiteral("path")).toString();
game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool();
game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool();
UISettings::values.game_dirs.append(game_dir);
}
qt_config->endArray();
// create NAND and SD card directories if empty, these are not removable through the UI,
// also carries over old game list settings if present
if (UISettings::values.game_dirs.isEmpty()) {
UISettings::GameDir game_dir;
game_dir.path = QStringLiteral("INSTALLED");
game_dir.expanded = true;
UISettings::values.game_dirs.append(game_dir);
game_dir.path = QStringLiteral("SYSTEM");
UISettings::values.game_dirs.append(game_dir);
if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) {
game_dir.path = UISettings::values.game_dir_deprecated;
game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan;
UISettings::values.game_dirs.append(game_dir);
}
}
UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
qt_config->endGroup(); qt_config->endGroup();
@ -899,10 +924,15 @@ void Config::SavePathValues() {
WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);
WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);
WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path);
WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, qt_config->beginWriteArray(QStringLiteral("gamedirs"));
QStringLiteral(".")); for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, qt_config->setArrayIndex(i);
false); const auto& game_dir = UISettings::values.game_dirs.at(i);
WriteSetting(QStringLiteral("path"), game_dir.path);
WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false);
WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
}
qt_config->endArray();
WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);
qt_config->endGroup(); qt_config->endGroup();

View file

@ -19,22 +19,17 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
} }
SetConfiguration(); SetConfiguration();
connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this,
[] { UISettings::values.is_game_list_reload_pending.exchange(true); });
} }
ConfigureGeneral::~ConfigureGeneral() = default; ConfigureGeneral::~ConfigureGeneral() = default;
void ConfigureGeneral::SetConfiguration() { void ConfigureGeneral::SetConfiguration() {
ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan);
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot); ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot);
ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
} }
void ConfigureGeneral::ApplyConfiguration() { void ConfigureGeneral::ApplyConfiguration() {
UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked();
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked(); UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked();
UISettings::values.theme = UISettings::values.theme =

View file

@ -24,13 +24,6 @@
<layout class="QHBoxLayout" name="GeneralHorizontalLayout"> <layout class="QHBoxLayout" name="GeneralHorizontalLayout">
<item> <item>
<layout class="QVBoxLayout" name="GeneralVerticalLayout"> <layout class="QVBoxLayout" name="GeneralVerticalLayout">
<item>
<widget class="QCheckBox" name="toggle_deepscan">
<property name="text">
<string>Search sub-directories for games</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="toggle_check_exit"> <widget class="QCheckBox" name="toggle_check_exit">
<property name="text"> <property name="text">

View file

@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
return QObject::eventFilter(obj, event); return QObject::eventFilter(obj, event);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
int rowCount = gamelist->tree_view->model()->rowCount();
QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower(); 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 the searchfield's text hasn't changed special function keys get checked
@ -56,19 +55,9 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
// If there is only one result launch this game // If there is only one result launch this game
case Qt::Key_Return: case Qt::Key_Return:
case Qt::Key_Enter: { case Qt::Key_Enter: {
QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view); if (gamelist->search_field->visible == 1) {
QModelIndex root_index = item_model->invisibleRootItem()->index(); QString file_path = gamelist->getLastFilterResultItem();
QStandardItem* child_file;
QString file_path;
int resultCount = 0;
for (int i = 0; i < rowCount; ++i) {
if (!gamelist->tree_view->isRowHidden(i, root_index)) {
++resultCount;
child_file = gamelist->item_model->item(i, 0);
file_path = child_file->data(GameListItemPath::FullPathRole).toString();
}
}
if (resultCount == 1) {
// To avoid loading error dialog loops while confirming them using enter // To avoid loading error dialog loops while confirming them using enter
// Also users usually want to run a different game after closing one // Also users usually want to run a different game after closing one
gamelist->search_field->edit_filter->clear(); gamelist->search_field->edit_filter->clear();
@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
} }
void GameListSearchField::setFilterResult(int visible, int total) { void GameListSearchField::setFilterResult(int visible, int total) {
this->visible = visible;
this->total = total;
label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
} }
QString GameList::getLastFilterResultItem() {
QStandardItem* folder;
QStandardItem* child;
QString file_path;
int folder_count = item_model->rowCount();
for (int i = 0; i < folder_count; ++i) {
folder = item_model->item(i, 0);
QModelIndex folder_index = folder->index();
int childrenCount = folder->rowCount();
for (int j = 0; j < childrenCount; ++j) {
if (!tree_view->isRowHidden(j, folder_index)) {
child = folder->child(j, 0);
file_path = child->data(GameListItemPath::FullPathRole).toString();
}
}
}
return file_path;
}
void GameListSearchField::clear() { void GameListSearchField::clear() {
edit_filter->clear(); edit_filter->clear();
} }
@ -147,45 +158,112 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)
[&haystack](const QString& s) { return haystack.contains(s); }); [&haystack](const QString& s) { return haystack.contains(s); });
} }
// Syncs the expanded state of Game Directories with settings to persist across sessions
void GameList::onItemExpanded(const QModelIndex& item) {
GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>();
if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir ||
type == GameListItemType::SystemDir)
item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded =
tree_view->isExpanded(item);
}
// Event in order to filter the gamelist after editing the searchfield // Event in order to filter the gamelist after editing the searchfield
void GameList::onTextChanged(const QString& new_text) { void GameList::onTextChanged(const QString& new_text) {
const int row_count = tree_view->model()->rowCount(); int folder_count = tree_view->model()->rowCount();
const QString edit_filter_text = new_text.toLower(); QString edit_filter_text = new_text.toLower();
const QModelIndex root_index = item_model->invisibleRootItem()->index(); QStandardItem* folder;
QStandardItem* child;
int childrenTotal = 0;
QModelIndex root_index = item_model->invisibleRootItem()->index();
// If the searchfield is empty every item is visible // If the searchfield is empty every item is visible
// Otherwise the filter gets applied // Otherwise the filter gets applied
if (edit_filter_text.isEmpty()) { if (edit_filter_text.isEmpty()) {
for (int i = 0; i < row_count; ++i) { for (int i = 0; i < folder_count; ++i) {
tree_view->setRowHidden(i, root_index, false); folder = item_model->item(i, 0);
QModelIndex folder_index = folder->index();
int childrenCount = folder->rowCount();
for (int j = 0; j < childrenCount; ++j) {
++childrenTotal;
tree_view->setRowHidden(j, folder_index, false);
}
} }
search_field->setFilterResult(row_count, row_count); search_field->setFilterResult(childrenTotal, childrenTotal);
} else { } else {
int result_count = 0; int result_count = 0;
for (int i = 0; i < row_count; ++i) { for (int i = 0; i < folder_count; ++i) {
const QStandardItem* child_file = item_model->item(i, 0); folder = item_model->item(i, 0);
const QString file_path = QModelIndex folder_index = folder->index();
child_file->data(GameListItemPath::FullPathRole).toString().toLower(); int childrenCount = folder->rowCount();
const QString file_title = for (int j = 0; j < childrenCount; ++j) {
child_file->data(GameListItemPath::TitleRole).toString().toLower(); ++childrenTotal;
const QString file_program_id = const QStandardItem* child = folder->child(j, 0);
child_file->data(GameListItemPath::ProgramIdRole).toString().toLower(); 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 =
child->data(GameListItemPath::ProgramIdRole).toString().toLower();
// Only items which filename in combination with its title contains all words // Only items which filename in combination with its title contains all words
// that are in the searchfield will be visible in the gamelist // that are in the searchfield will be visible in the gamelist
// The search is case insensitive because of toLower() // The search is case insensitive because of toLower()
// I decided not to use Qt::CaseInsensitive in containsAllWords to prevent // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
// multiple conversions of edit_filter_text for each game in the gamelist // multiple conversions of edit_filter_text for each game in the gamelist
const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + const QString file_name =
QLatin1Char{' '} + file_title; file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
if (ContainsAllWords(file_name, edit_filter_text) || file_title;
(file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { if (ContainsAllWords(file_name, edit_filter_text) ||
tree_view->setRowHidden(i, root_index, false); (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
++result_count; tree_view->setRowHidden(j, folder_index, false);
} else { ++result_count;
tree_view->setRowHidden(i, root_index, true); } else {
tree_view->setRowHidden(j, folder_index, true);
}
search_field->setFilterResult(result_count, childrenTotal);
} }
search_field->setFilterResult(result_count, row_count); }
}
}
void GameList::onUpdateThemedIcons() {
for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
QStandardItem* child = item_model->invisibleRootItem()->child(i);
int icon_size = UISettings::values.icon_size;
switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::InstalledDir:
child->setData(
QIcon::fromTheme(QStringLiteral("sd_card"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
break;
case GameListItemType::SystemDir:
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 =
child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
QString icon_name = QFileInfo::exists(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("plus"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
break;
} }
} }
} }
@ -230,12 +308,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type"));
item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size"));
} }
item_model->setSortRole(GameListItemPath::TitleRole);
connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);
connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
// We must register all custom types with the Qt Automoc system so that we are able to use it // We must register all custom types with the Qt Automoc system so that we are able to use
// with signals/slots. In this case, QList falls under the umbrells of custom types. // it with signals/slots. In this case, QList falls under the umbrells of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
@ -263,38 +345,67 @@ void GameList::clearFilter() {
search_field->clear(); search_field->clear();
} }
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { void GameList::AddDirEntry(GameListDir* entry_items) {
item_model->invisibleRootItem()->appendRow(entry_items); item_model->invisibleRootItem()->appendRow(entry_items);
tree_view->setExpanded(
entry_items->index(),
entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
}
void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
parent->appendRow(entry_items);
} }
void GameList::ValidateEntry(const QModelIndex& item) { void GameList::ValidateEntry(const QModelIndex& item) {
// We don't care about the individual QStandardItem that was selected, but its row. auto selected = item.sibling(item.row(), 0);
const int row = item_model->itemFromIndex(item)->row();
const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString();
if (file_path.isEmpty()) switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
return; case GameListItemType::Game: {
QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
if (file_path.isEmpty())
return;
QFileInfo file_info(file_path);
if (!file_info.exists())
return;
if (!QFileInfo::exists(file_path)) if (file_info.isDir()) {
return; const QDir dir{file_path};
const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
const QFileInfo file_info{file_path}; if (matching_main.size() == 1) {
if (file_info.isDir()) { emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
const QDir dir{file_path}; }
const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); return;
if (matching_main.size() == 1) {
emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
} }
return;
}
// Users usually want to run a diffrent game after closing one // Users usually want to run a different game after closing one
search_field->clear(); search_field->clear();
emit GameChosen(file_path); emit GameChosen(file_path);
break;
}
case GameListItemType::AddDir:
emit AddDirectory();
break;
}
}
bool GameList::isEmpty() {
for (int i = 0; i < item_model->rowCount(); i++) {
const QStandardItem* child = item_model->invisibleRootItem()->child(i);
GameListItemType type = static_cast<GameListItemType>(child->type());
if (!child->hasChildren() &&
(type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) {
item_model->invisibleRootItem()->removeRow(child->row());
i--;
};
}
return !item_model->invisibleRootItem()->hasChildren();
} }
void GameList::DonePopulating(QStringList watch_list) { void GameList::DonePopulating(QStringList watch_list) {
emit ShowList(!isEmpty());
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
// Clear out the old directories to watch for changes and add the new ones // Clear out the old directories to watch for changes and add the new ones
auto watch_dirs = watcher->directories(); auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) { if (!watch_dirs.isEmpty()) {
@ -311,9 +422,16 @@ void GameList::DonePopulating(QStringList watch_list) {
QCoreApplication::processEvents(); QCoreApplication::processEvents();
} }
tree_view->setEnabled(true); tree_view->setEnabled(true);
int rowCount = tree_view->model()->rowCount(); int folder_count = tree_view->model()->rowCount();
search_field->setFilterResult(rowCount, rowCount); int childrenTotal = 0;
if (rowCount > 0) { for (int i = 0; i < folder_count; ++i) {
int childrenCount = item_model->item(i, 0)->rowCount();
for (int j = 0; j < childrenCount; ++j) {
++childrenTotal;
}
}
search_field->setFilterResult(childrenTotal, childrenTotal);
if (childrenTotal > 0) {
search_field->setFocus(); search_field->setFocus();
} }
} }
@ -323,12 +441,26 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
if (!item.isValid()) if (!item.isValid())
return; return;
int row = item_model->itemFromIndex(item)->row(); auto selected = item.sibling(item.row(), 0);
QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong();
std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString();
QMenu context_menu; QMenu context_menu;
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::Game:
AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
selected.data(GameListItemPath::FullPathRole).toString().toStdString());
break;
case GameListItemType::CustomDir:
AddPermDirPopup(context_menu, selected);
AddCustomDirPopup(context_menu, selected);
break;
case GameListItemType::InstalledDir:
case GameListItemType::SystemDir:
AddPermDirPopup(context_menu, selected);
break;
}
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
}
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) {
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));
QAction* open_transferable_shader_cache = QAction* open_transferable_shader_cache =
@ -344,19 +476,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
connect(open_save_location, &QAction::triggered, connect(open_save_location, &QAction::triggered, [this, program_id]() {
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData);
connect(open_lfs_location, &QAction::triggered, });
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); connect(open_lfs_location, &QAction::triggered, [this, program_id]() {
emit OpenFolderRequested(program_id, GameListOpenTarget::ModData);
});
connect(open_transferable_shader_cache, &QAction::triggered, connect(open_transferable_shader_cache, &QAction::triggered,
[&]() { emit OpenTransferableShaderCacheRequested(program_id); }); [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); connect(dump_romfs, &QAction::triggered,
connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); });
connect(navigate_to_gamedb_entry, &QAction::triggered, connect(copy_tid, &QAction::triggered,
[&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); [this, program_id]() { emit CopyTIDRequested(program_id); });
connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
});
connect(properties, &QAction::triggered,
[this, path]() { emit OpenPerGameGeneralRequested(path); });
};
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
UISettings::GameDir& game_dir =
*selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
deep_scan->setCheckable(true);
deep_scan->setChecked(game_dir.deep_scan);
connect(deep_scan, &QAction::triggered, [this, &game_dir] {
game_dir.deep_scan = !game_dir.deep_scan;
PopulateAsync(UISettings::values.game_dirs);
});
connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] {
UISettings::values.game_dirs.removeOne(game_dir);
item_model->invisibleRootItem()->removeRow(selected.row());
});
}
void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
UISettings::GameDir& game_dir =
*selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up"));
QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down "));
QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location"));
int row = selected.row();
move_up->setEnabled(row > 0);
move_down->setEnabled(row < item_model->rowCount() - 2);
connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] {
// find the indices of the items in settings and swap them
UISettings::values.game_dirs.swap(
UISettings::values.game_dirs.indexOf(game_dir),
UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() - 1, 0)
.data(GameListDir::GameDirRole)
.value<UISettings::GameDir*>()));
// move the treeview items
QList<QStandardItem*> item = item_model->takeRow(row);
item_model->invisibleRootItem()->insertRow(row - 1, item);
tree_view->setExpanded(selected, game_dir.expanded);
});
connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] {
// find the indices of the items in settings and swap them
UISettings::values.game_dirs.swap(
UISettings::values.game_dirs.indexOf(game_dir),
UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() + 1, 0)
.data(GameListDir::GameDirRole)
.value<UISettings::GameDir*>()));
// move the treeview items
QList<QStandardItem*> item = item_model->takeRow(row);
item_model->invisibleRootItem()->insertRow(row + 1, item);
tree_view->setExpanded(selected, game_dir.expanded);
});
connect(open_directory_location, &QAction::triggered,
[this, game_dir] { emit OpenDirectory(game_dir.path); });
} }
void GameList::LoadCompatibilityList() { void GameList::LoadCompatibilityList() {
@ -403,14 +602,7 @@ void GameList::LoadCompatibilityList() {
} }
} }
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) {
const QFileInfo dir_info{dir_path};
if (!dir_info.exists() || !dir_info.isDir()) {
LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString());
search_field->setFilterResult(0, 0);
return;
}
tree_view->setEnabled(false); tree_view->setEnabled(false);
// Update the columns in case UISettings has changed // Update the columns in case UISettings has changed
@ -433,17 +625,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
// Delete any rows that might already exist if we're repopulating // Delete any rows that might already exist if we're repopulating
item_model->removeRows(0, item_model->rowCount()); item_model->removeRows(0, item_model->rowCount());
search_field->clear();
emit ShouldCancelWorker(); emit ShouldCancelWorker();
GameListWorker* worker = GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);
new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
Qt::QueuedConnection);
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
Qt::QueuedConnection); Qt::QueuedConnection);
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
// without delay. // cancel without delay.
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
Qt::DirectConnection); Qt::DirectConnection);
@ -471,10 +665,42 @@ const QStringList GameList::supported_file_extensions = {
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
void GameList::RefreshGameDirectory() { void GameList::RefreshGameDirectory() {
if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
search_field->clear(); PopulateAsync(UISettings::values.game_dirs);
PopulateAsync(UISettings::values.game_directory_path,
UISettings::values.game_directory_deepscan);
} }
} }
GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
this->main_window = parent;
connect(main_window, &GMainWindow::UpdateThemedIcons, this,
&GameListPlaceholder::onUpdateThemedIcons);
layout = new QVBoxLayout;
image = new QLabel;
text = new QLabel;
layout->setAlignment(Qt::AlignCenter);
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
text->setText(tr("Double-click to add a new folder to the game list "));
QFont font = text->font();
font.setPointSize(20);
text->setFont(font);
text->setAlignment(Qt::AlignHCenter);
image->setAlignment(Qt::AlignHCenter);
layout->addWidget(image);
layout->addWidget(text);
setLayout(layout);
}
GameListPlaceholder::~GameListPlaceholder() = default;
void GameListPlaceholder::onUpdateThemedIcons() {
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
}
void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
emit GameListPlaceholder::AddDirectory();
}

View file

@ -19,10 +19,14 @@
#include <QWidget> #include <QWidget>
#include "common/common_types.h" #include "common/common_types.h"
#include "ui_settings.h"
#include "yuzu/compatibility_list.h" #include "yuzu/compatibility_list.h"
class GameListWorker; class GameListWorker;
class GameListSearchField; class GameListSearchField;
template <typename>
class QList;
class GameListDir;
class GMainWindow; class GMainWindow;
namespace FileSys { namespace FileSys {
@ -52,12 +56,14 @@ public:
FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr); FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
~GameList() override; ~GameList() override;
QString getLastFilterResultItem();
void clearFilter(); void clearFilter();
void setFilterFocus(); void setFilterFocus();
void setFilterVisible(bool visibility); void setFilterVisible(bool visibility);
bool isEmpty();
void LoadCompatibilityList(); void LoadCompatibilityList();
void PopulateAsync(const QString& dir_path, bool deep_scan); void PopulateAsync(QList<UISettings::GameDir>& game_dirs);
void SaveInterfaceLayout(); void SaveInterfaceLayout();
void LoadInterfaceLayout(); void LoadInterfaceLayout();
@ -74,19 +80,29 @@ signals:
void NavigateToGamedbEntryRequested(u64 program_id, void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const std::string& file); void OpenPerGameGeneralRequested(const std::string& file);
void OpenDirectory(QString directory);
void AddDirectory();
void ShowList(bool show);
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();
private: private:
void AddEntry(const QList<QStandardItem*>& entry_items); void AddDirEntry(GameListDir* entry_items);
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
void ValidateEntry(const QModelIndex& item); void ValidateEntry(const QModelIndex& item);
void DonePopulating(QStringList watch_list); void DonePopulating(QStringList watch_list);
void PopupContextMenu(const QPoint& menu_location);
void RefreshGameDirectory(); void RefreshGameDirectory();
void PopupContextMenu(const QPoint& menu_location);
void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path);
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
std::shared_ptr<FileSys::VfsFilesystem> vfs; std::shared_ptr<FileSys::VfsFilesystem> vfs;
FileSys::ManualContentProvider* provider; FileSys::ManualContentProvider* provider;
GameListSearchField* search_field; GameListSearchField* search_field;
@ -102,3 +118,25 @@ private:
}; };
Q_DECLARE_METATYPE(GameListOpenTarget); Q_DECLARE_METATYPE(GameListOpenTarget);
class GameListPlaceholder : public QWidget {
Q_OBJECT
public:
explicit GameListPlaceholder(GMainWindow* parent = nullptr);
~GameListPlaceholder();
signals:
void AddDirectory();
private slots:
void onUpdateThemedIcons();
protected:
void mouseDoubleClickEvent(QMouseEvent* event) override;
private:
GMainWindow* main_window = nullptr;
QVBoxLayout* layout = nullptr;
QLabel* image = nullptr;
QLabel* text = nullptr;
};

View file

@ -10,6 +10,7 @@
#include <utility> #include <utility>
#include <QCoreApplication> #include <QCoreApplication>
#include <QFileInfo>
#include <QImage> #include <QImage>
#include <QObject> #include <QObject>
#include <QStandardItem> #include <QStandardItem>
@ -22,6 +23,16 @@
#include "yuzu/uisettings.h" #include "yuzu/uisettings.h"
#include "yuzu/util/util.h" #include "yuzu/util/util.h"
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
CustomDir = QStandardItem::UserType + 2,
InstalledDir = QStandardItem::UserType + 3,
SystemDir = QStandardItem::UserType + 4,
AddDir = QStandardItem::UserType + 5
};
Q_DECLARE_METATYPE(GameListItemType);
/** /**
* Gets the default icon (for games without valid title metadata) * Gets the default icon (for games without valid title metadata)
* @param size The desired width and height of the default icon. * @param size The desired width and height of the default icon.
@ -36,8 +47,13 @@ static QPixmap GetDefaultIcon(u32 size) {
class GameListItem : public QStandardItem { class GameListItem : public QStandardItem {
public: public:
// used to access type from item index
static const int TypeRole = Qt::UserRole + 1;
static const int SortRole = Qt::UserRole + 2;
GameListItem() = default; GameListItem() = default;
explicit GameListItem(const QString& string) : QStandardItem(string) {} GameListItem(const QString& string) : QStandardItem(string) {
setData(string, SortRole);
}
}; };
/** /**
@ -48,14 +64,15 @@ public:
*/ */
class GameListItemPath : public GameListItem { class GameListItemPath : public GameListItem {
public: public:
static const int FullPathRole = Qt::UserRole + 1; static const int TitleRole = SortRole;
static const int TitleRole = Qt::UserRole + 2; static const int FullPathRole = SortRole + 1;
static const int ProgramIdRole = Qt::UserRole + 3; static const int ProgramIdRole = SortRole + 2;
static const int FileTypeRole = Qt::UserRole + 4; static const int FileTypeRole = SortRole + 3;
GameListItemPath() = default; GameListItemPath() = default;
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
const QString& game_name, const QString& game_type, u64 program_id) { const QString& game_name, const QString& game_type, u64 program_id) {
setData(type(), TypeRole);
setData(game_path, FullPathRole); setData(game_path, FullPathRole);
setData(game_name, TitleRole); setData(game_name, TitleRole);
setData(qulonglong(program_id), ProgramIdRole); setData(qulonglong(program_id), ProgramIdRole);
@ -72,6 +89,10 @@ public:
setData(picture, Qt::DecorationRole); setData(picture, Qt::DecorationRole);
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
QVariant data(int role) const override { QVariant data(int role) const override {
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
std::string filename; std::string filename;
@ -103,9 +124,11 @@ public:
class GameListItemCompat : public GameListItem { class GameListItemCompat : public GameListItem {
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
public: public:
static const int CompatNumberRole = Qt::UserRole + 1; static const int CompatNumberRole = SortRole;
GameListItemCompat() = default; GameListItemCompat() = default;
explicit GameListItemCompat(const QString& compatibility) { explicit GameListItemCompat(const QString& compatibility) {
setData(type(), TypeRole);
struct CompatStatus { struct CompatStatus {
QString color; QString color;
const char* text; const char* text;
@ -135,6 +158,10 @@ public:
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
bool operator<(const QStandardItem& other) const override { bool operator<(const QStandardItem& other) const override {
return data(CompatNumberRole) < other.data(CompatNumberRole); return data(CompatNumberRole) < other.data(CompatNumberRole);
} }
@ -146,12 +173,12 @@ public:
* human-readable string representation will be displayed to the user. * human-readable string representation will be displayed to the user.
*/ */
class GameListItemSize : public GameListItem { class GameListItemSize : public GameListItem {
public: public:
static const int SizeRole = Qt::UserRole + 1; static const int SizeRole = SortRole;
GameListItemSize() = default; GameListItemSize() = default;
explicit GameListItemSize(const qulonglong size_bytes) { explicit GameListItemSize(const qulonglong size_bytes) {
setData(type(), TypeRole);
setData(size_bytes, SizeRole); setData(size_bytes, SizeRole);
} }
@ -167,6 +194,10 @@ public:
} }
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
/** /**
* This operator is, in practice, only used by the TreeView sorting systems. * 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 * Override it so that it will correctly sort by numerical value instead of by string
@ -177,6 +208,67 @@ public:
} }
}; };
class GameListDir : public GameListItem {
public:
static const 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::fromValue(game_dir), GameDirRole);
int icon_size = UISettings::values.icon_size;
switch (dir_type) {
case GameListItemType::InstalledDir:
setData(QIcon::fromTheme("sd_card").pixmap(icon_size).scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData("Installed Titles", Qt::DisplayRole);
break;
case GameListItemType::SystemDir:
setData(QIcon::fromTheme("chip").pixmap(icon_size).scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData("System Titles", Qt::DisplayRole);
break;
case GameListItemType::CustomDir:
QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(game_dir->path, Qt::DisplayRole);
break;
};
};
int type() const override {
return static_cast<int>(dir_type);
}
private:
GameListItemType dir_type;
};
class GameListAddDir : public GameListItem {
public:
explicit GameListAddDir() {
setData(type(), TypeRole);
int icon_size = UISettings::values.icon_size;
setData(QIcon::fromTheme("plus").pixmap(icon_size).scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData("Add New Game Directory", Qt::DisplayRole);
}
int type() const override {
return static_cast<int>(GameListItemType::AddDir);
}
};
class GameList; class GameList;
class QHBoxLayout; class QHBoxLayout;
class QTreeView; class QTreeView;
@ -195,6 +287,9 @@ public:
void clear(); void clear();
void setFocus(); void setFocus();
int visible;
int total;
private: private:
class KeyReleaseEater : public QObject { class KeyReleaseEater : public QObject {
public: public:

View file

@ -223,21 +223,38 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
} // Anonymous namespace } // Anonymous namespace
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
FileSys::ManualContentProvider* provider, QString dir_path, FileSys::ManualContentProvider* provider,
bool deep_scan, const CompatibilityList& compatibility_list) QList<UISettings::GameDir>& game_dirs,
: vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan), const CompatibilityList& compatibility_list)
: vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),
compatibility_list(compatibility_list) {} compatibility_list(compatibility_list) {}
GameListWorker::~GameListWorker() = default; GameListWorker::~GameListWorker() = default;
void GameListWorker::AddTitlesToGameList() { void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>( using namespace FileSys;
Core::System::GetInstance().GetContentProvider());
const auto installed_games = cache.ListEntriesFilterOrigin( const auto& cache =
std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider());
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games;
installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
ContentRecordType::Program);
if (parent_dir->type() == static_cast<int>(GameListItemType::InstalledDir)) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
auto installed_sdmc_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
installed_games.insert(installed_games.end(), installed_sdmc_games.begin(),
installed_sdmc_games.end());
} else if (parent_dir->type() == static_cast<int>(GameListItemType::SystemDir)) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
}
for (const auto& [slot, game] : installed_games) { for (const auto& [slot, game] : installed_games) {
if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) if (slot == ContentProviderUnionSlot::FrontendManual)
continue; continue;
const auto file = cache.GetEntryUnparsed(game.title_id, game.type); const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
@ -250,21 +267,22 @@ void GameListWorker::AddTitlesToGameList() {
u64 program_id = 0; u64 program_id = 0;
loader->ReadProgramId(program_id); loader->ReadProgramId(program_id);
const FileSys::PatchManager patch{program_id}; const PatchManager patch{program_id};
const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
if (control != nullptr) if (control != nullptr)
GetMetadataFromControlNCA(patch, *control, icon, name); GetMetadataFromControlNCA(patch, *control, icon, name);
emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
compatibility_list, patch)); compatibility_list, patch),
parent_dir);
} }
} }
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
unsigned int recursion) { unsigned int recursion, GameListDir* parent_dir) {
const auto callback = [this, target, recursion](u64* num_entries_out, const auto callback = [this, target, recursion,
const std::string& directory, parent_dir](u64* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool { const std::string& virtual_name) -> bool {
if (stop_processing) { if (stop_processing) {
// Breaks the callback loop. // Breaks the callback loop.
return false; return false;
@ -317,11 +335,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{program_id}; const FileSys::PatchManager patch{program_id};
emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
compatibility_list, patch)); compatibility_list, patch),
parent_dir);
} }
} else if (is_dir && recursion > 0) { } else if (is_dir && recursion > 0) {
watch_list.append(QString::fromStdString(physical_name)); watch_list.append(QString::fromStdString(physical_name));
ScanFileSystem(target, physical_name, recursion - 1); ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
} }
return true; return true;
@ -332,12 +351,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
void GameListWorker::run() { void GameListWorker::run() {
stop_processing = false; stop_processing = false;
watch_list.append(dir_path);
provider->ClearAllEntries(); for (UISettings::GameDir& game_dir : game_dirs) {
ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), if (game_dir.path == "INSTALLED") {
deep_scan ? 256 : 0); GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
AddTitlesToGameList(); emit DirEntryReady({game_list_dir});
ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); AddTitlesToGameList(game_list_dir);
} else if (game_dir.path == "SYSTEM") {
GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
emit DirEntryReady({game_list_dir});
AddTitlesToGameList(game_list_dir);
} else {
watch_list.append(game_dir.path);
GameListDir* game_list_dir = new GameListDir(game_dir);
emit DirEntryReady({game_list_dir});
provider->ClearAllEntries();
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2,
game_list_dir);
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
game_dir.deep_scan ? 256 : 0, game_list_dir);
}
};
emit Finished(watch_list); emit Finished(watch_list);
} }

View file

@ -33,9 +33,10 @@ class GameListWorker : public QObject, public QRunnable {
Q_OBJECT Q_OBJECT
public: public:
GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, FileSys::ManualContentProvider* provider,
const CompatibilityList& compatibility_list); QList<UISettings::GameDir>& game_dirs,
const CompatibilityList& compatibility_list);
~GameListWorker() override; ~GameListWorker() override;
/// Starts the processing of directory tree information. /// Starts the processing of directory tree information.
@ -48,31 +49,33 @@ signals:
/** /**
* The `EntryReady` signal is emitted once an entry has been prepared and is ready * The `EntryReady` signal is emitted once an entry has been prepared and is ready
* to be added to the game list. * to be added to the game list.
* @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. * @param entry_items a list with `QStandardItem`s that make up the columns of the new
* entry.
*/ */
void EntryReady(QList<QStandardItem*> entry_items); void DirEntryReady(GameListDir* entry_items);
void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
/** /**
* After the worker has traversed the game directory looking for entries, this signal is emitted * After the worker has traversed the game directory looking for entries, this signal is
* with a list of folders that should be watched for changes as well. * emitted with a list of folders that should be watched for changes as well.
*/ */
void Finished(QStringList watch_list); void Finished(QStringList watch_list);
private: private:
void AddTitlesToGameList(); void AddTitlesToGameList(GameListDir* parent_dir);
enum class ScanTarget { enum class ScanTarget {
FillManualContentProvider, FillManualContentProvider,
PopulateGameList, PopulateGameList,
}; };
void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion,
GameListDir* parent_dir);
std::shared_ptr<FileSys::VfsFilesystem> vfs; std::shared_ptr<FileSys::VfsFilesystem> vfs;
FileSys::ManualContentProvider* provider; FileSys::ManualContentProvider* provider;
QStringList watch_list; QStringList watch_list;
QString dir_path;
bool deep_scan;
const CompatibilityList& compatibility_list; const CompatibilityList& compatibility_list;
QList<UISettings::GameDir>& game_dirs;
std::atomic_bool stop_processing; std::atomic_bool stop_processing;
}; };

View file

@ -216,8 +216,7 @@ GMainWindow::GMainWindow()
OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
game_list->LoadCompatibilityList(); game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
// Show one-time "callout" messages to the user // Show one-time "callout" messages to the user
ShowTelemetryCallout(); ShowTelemetryCallout();
@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() {
game_list = new GameList(vfs, provider.get(), this); game_list = new GameList(vfs, provider.get(), this);
ui.horizontalLayout->addWidget(game_list); ui.horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
ui.horizontalLayout->addWidget(game_list_placeholder);
game_list_placeholder->setVisible(false);
loading_screen = new LoadingScreen(this); loading_screen = new LoadingScreen(this);
loading_screen->hide(); loading_screen->hide();
ui.horizontalLayout->addWidget(loading_screen); ui.horizontalLayout->addWidget(loading_screen);
@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() {
void GMainWindow::ConnectWidgetEvents() { void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,
&GMainWindow::OnTransferableShaderCacheOpenFile); &GMainWindow::OnTransferableShaderCacheOpenFile);
@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry); &GMainWindow::OnGameListNavigateToGamedbEntry);
connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
&GMainWindow::OnGameListAddDirectory);
connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
connect(game_list, &GameList::OpenPerGameGeneralRequested, this, connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
&GMainWindow::OnGameListOpenPerGameProperties); &GMainWindow::OnGameListOpenPerGameProperties);
@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
connect(ui.action_Install_File_NAND, &QAction::triggered, this, connect(ui.action_Install_File_NAND, &QAction::triggered, this,
&GMainWindow::OnMenuInstallToNAND); &GMainWindow::OnMenuInstallToNAND);
connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
&GMainWindow::OnMenuSelectGameListRoot);
connect(ui.action_Select_NAND_Directory, &QAction::triggered, this, connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
[this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); }); [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) {
// Update the GUI // Update the GUI
if (ui.action_Single_Window_Mode->isChecked()) { if (ui.action_Single_Window_Mode->isChecked()) {
game_list->hide(); game_list->hide();
game_list_placeholder->hide();
} }
status_bar_update_timer.start(2000); status_bar_update_timer.start(2000);
@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() {
render_window->hide(); render_window->hide();
loading_screen->hide(); loading_screen->hide();
loading_screen->Clear(); loading_screen->Clear();
game_list->show(); if (game_list->isEmpty())
game_list_placeholder->show();
else
game_list->show();
game_list->setFilterFocus(); game_list->setFilterFocus();
UpdateWindowTitle(); UpdateWindowTitle();
@ -1298,6 +1309,45 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
} }
void GMainWindow::OnGameListOpenDirectory(QString directory) {
QString path;
if (directory == QStringLiteral("INSTALLED")) {
// TODO: Find a better solution when installing files to the SD card gets implemented
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() +
std::string("user/Contents/registered"));
} else if (directory == QStringLiteral("SYSTEM")) {
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() +
std::string("system/Contents/registered"));
} else {
path = directory;
}
if (!QFileInfo::exists(path)) {
QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
return;
}
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
}
void GMainWindow::OnGameListAddDirectory() {
QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
if (dir_path.isEmpty())
return;
UISettings::GameDir game_dir{dir_path, false, true};
if (!UISettings::values.game_dirs.contains(game_dir)) {
UISettings::values.game_dirs.append(game_dir);
game_list->PopulateAsync(UISettings::values.game_dirs);
} else {
LOG_WARNING(Frontend, "Selected directory is already in the game list");
}
}
void GMainWindow::OnGameListShowList(bool show) {
if (emulation_running && ui.action_Single_Window_Mode->isChecked())
return;
game_list->setVisible(show);
game_list_placeholder->setVisible(!show);
};
void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
u64 title_id{}; u64 title_id{};
const auto v_file = Core::GetGameFileFromPath(vfs, file); const auto v_file = Core::GetGameFileFromPath(vfs, file);
@ -1316,8 +1366,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
if (reload) { if (reload) {
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
} }
config->Save(); config->Save();
@ -1407,8 +1456,7 @@ void GMainWindow::OnMenuInstallToNAND() {
const auto success = [this]() { const auto success = [this]() {
QMessageBox::information(this, tr("Successfully Installed"), QMessageBox::information(this, tr("Successfully Installed"),
tr("The file was successfully installed.")); tr("The file was successfully installed."));
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
DIR_SEP + "game_list"); DIR_SEP + "game_list");
}; };
@ -1533,14 +1581,6 @@ void GMainWindow::OnMenuInstallToNAND() {
} }
} }
void GMainWindow::OnMenuSelectGameListRoot() {
QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
if (!dir_path.isEmpty()) {
UISettings::values.game_directory_path = dir_path;
game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan);
}
}
void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
const auto res = QMessageBox::information( const auto res = QMessageBox::information(
this, tr("Changing Emulated Directory"), this, tr("Changing Emulated Directory"),
@ -1559,8 +1599,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)
: FileUtil::UserPath::NANDDir, : FileUtil::UserPath::NANDDir,
dir_path.toStdString()); dir_path.toStdString());
Service::FileSystem::CreateFactories(*vfs); Service::FileSystem::CreateFactories(*vfs);
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
} }
} }
@ -1724,11 +1763,11 @@ void GMainWindow::OnConfigure() {
if (UISettings::values.enable_discord_presence != old_discord_presence) { if (UISettings::values.enable_discord_presence != old_discord_presence) {
SetDiscordEnabled(UISettings::values.enable_discord_presence); SetDiscordEnabled(UISettings::values.enable_discord_presence);
} }
emit UpdateThemedIcons();
const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
if (reload) { if (reload) {
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
} }
config->Save(); config->Save();
@ -1992,8 +2031,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
Service::FileSystem::CreateFactories(*vfs); Service::FileSystem::CreateFactories(*vfs);
if (behavior == ReinitializeKeyBehavior::Warning) { if (behavior == ReinitializeKeyBehavior::Warning) {
game_list->PopulateAsync(UISettings::values.game_directory_path, game_list->PopulateAsync(UISettings::values.game_dirs);
UISettings::values.game_directory_deepscan);
} }
} }
@ -2158,7 +2196,6 @@ void GMainWindow::UpdateUITheme() {
} }
QIcon::setThemeSearchPaths(theme_paths); QIcon::setThemeSearchPaths(theme_paths);
emit UpdateThemedIcons();
} }
void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {

View file

@ -30,6 +30,7 @@ class ProfilerWidget;
class QLabel; class QLabel;
class WaitTreeWidget; class WaitTreeWidget;
enum class GameListOpenTarget; enum class GameListOpenTarget;
class GameListPlaceholder;
namespace Core::Frontend { namespace Core::Frontend {
struct SoftwareKeyboardParameters; struct SoftwareKeyboardParameters;
@ -186,12 +187,13 @@ private slots:
void OnGameListCopyTID(u64 program_id); void OnGameListCopyTID(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id, void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);
void OnGameListOpenDirectory(QString path);
void OnGameListAddDirectory();
void OnGameListShowList(bool show);
void OnGameListOpenPerGameProperties(const std::string& file); void OnGameListOpenPerGameProperties(const std::string& file);
void OnMenuLoadFile(); void OnMenuLoadFile();
void OnMenuLoadFolder(); void OnMenuLoadFolder();
void OnMenuInstallToNAND(); void OnMenuInstallToNAND();
/// Called whenever a user selects the "File->Select Game List Root" menu item
void OnMenuSelectGameListRoot();
/// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card
void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target);
void OnMenuRecentFile(); void OnMenuRecentFile();
@ -223,6 +225,8 @@ private:
GameList* game_list; GameList* game_list;
LoadingScreen* loading_screen; LoadingScreen* loading_screen;
GameListPlaceholder* game_list_placeholder;
// Status bar elements // Status bar elements
QLabel* message_label = nullptr; QLabel* message_label = nullptr;
QLabel* emu_speed_label = nullptr; QLabel* emu_speed_label = nullptr;

View file

@ -62,7 +62,6 @@
<addaction name="action_Load_File"/> <addaction name="action_Load_File"/>
<addaction name="action_Load_Folder"/> <addaction name="action_Load_Folder"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_Select_Game_List_Root"/>
<addaction name="menu_recent_files"/> <addaction name="menu_recent_files"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_Select_NAND_Directory"/> <addaction name="action_Select_NAND_Directory"/>

View file

@ -8,6 +8,7 @@
#include <atomic> #include <atomic>
#include <vector> #include <vector>
#include <QByteArray> #include <QByteArray>
#include <QMetaType>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include "common/common_types.h" #include "common/common_types.h"
@ -25,6 +26,18 @@ struct Shortcut {
using Themes = std::array<std::pair<const char*, const char*>, 2>; using Themes = std::array<std::pair<const char*, const char*>, 2>;
extern const Themes themes; extern const Themes themes;
struct GameDir {
QString path;
bool deep_scan;
bool expanded;
bool operator==(const GameDir& rhs) const {
return path == rhs.path;
};
bool operator!=(const GameDir& rhs) const {
return !operator==(rhs);
};
};
struct Values { struct Values {
QByteArray geometry; QByteArray geometry;
QByteArray state; QByteArray state;
@ -55,8 +68,9 @@ struct Values {
QString roms_path; QString roms_path;
QString symbols_path; QString symbols_path;
QString screenshot_path; QString screenshot_path;
QString game_directory_path; QString game_dir_deprecated;
bool game_directory_deepscan; bool game_dir_deprecated_deepscan;
QList<UISettings::GameDir> game_dirs;
QStringList recent_files; QStringList recent_files;
QString theme; QString theme;
@ -84,3 +98,5 @@ struct Values {
extern Values values; extern Values values;
} // namespace UISettings } // namespace UISettings
Q_DECLARE_METATYPE(UISettings::GameDir*);