summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorfearlessTobi <thm.frey@gmail.com>2019-05-01 23:21:04 +0200
committerFearlessTobi <thm.frey@gmail.com>2019-09-04 16:47:32 +0200
commit2d8eba5bafd7fe9da00c8a57c605a503c3ece478 (patch)
treec3bc64c33ab43f6bd0d7bb6f4ec63b11490a41e8 /src
parentAdd assets and licenses (diff)
downloadyuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.gz
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.bz2
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.lz
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.xz
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.zst
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.zip
Diffstat (limited to 'src')
-rw-r--r--src/yuzu/configuration/config.cpp42
-rw-r--r--src/yuzu/configuration/configure_general.cpp5
-rw-r--r--src/yuzu/configuration/configure_general.ui7
-rw-r--r--src/yuzu/game_list.cpp430
-rw-r--r--src/yuzu/game_list.h44
-rw-r--r--src/yuzu/game_list_p.h111
-rw-r--r--src/yuzu/game_list_worker.cpp83
-rw-r--r--src/yuzu/game_list_worker.h25
-rw-r--r--src/yuzu/main.cpp85
-rw-r--r--src/yuzu/main.h8
-rw-r--r--src/yuzu/main.ui1
-rw-r--r--src/yuzu/uisettings.h20
12 files changed, 666 insertions, 195 deletions
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 0456248ac..f2f116a87 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -517,10 +517,35 @@ void Config::ReadPathValues() {
UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString();
UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString();
UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString();
- UISettings::values.game_directory_path =
+ UISettings::values.game_dir_deprecated =
ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString();
- UISettings::values.game_directory_deepscan =
+ UISettings::values.game_dir_deprecated_deepscan =
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();
qt_config->endGroup();
@@ -899,10 +924,15 @@ void Config::SavePathValues() {
WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);
WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);
WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path);
- WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path,
- QStringLiteral("."));
- WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan,
- false);
+ qt_config->beginWriteArray(QStringLiteral("gamedirs"));
+ for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
+ qt_config->setArrayIndex(i);
+ 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);
qt_config->endGroup();
diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp
index 75fcbfea3..727836b17 100644
--- a/src/yuzu/configuration/configure_general.cpp
+++ b/src/yuzu/configuration/configure_general.cpp
@@ -19,22 +19,17 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
}
SetConfiguration();
-
- connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this,
- [] { UISettings::values.is_game_list_reload_pending.exchange(true); });
}
ConfigureGeneral::~ConfigureGeneral() = default;
void ConfigureGeneral::SetConfiguration() {
- ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan);
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot);
ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
}
void ConfigureGeneral::ApplyConfiguration() {
- UISettings::values.game_directory_deepscan = ui->toggle_deepscan->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.theme =
diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui
index 184fdd329..e747a4ce2 100644
--- a/src/yuzu/configuration/configure_general.ui
+++ b/src/yuzu/configuration/configure_general.ui
@@ -25,13 +25,6 @@
<item>
<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>
<widget class="QCheckBox" name="toggle_check_exit">
<property name="text">
<string>Confirm exit while emulation is running</string>
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index d18b96519..65947c59b 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
return QObject::eventFilter(obj, 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();
// 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
case Qt::Key_Return:
case Qt::Key_Enter: {
- QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view);
- QModelIndex root_index = item_model->invisibleRootItem()->index();
- 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) {
+ if (gamelist->search_field->visible == 1) {
+ 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();
@@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
}
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));
}
+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() {
edit_filter->clear();
}
@@ -147,45 +158,112 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)
[&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
void GameList::onTextChanged(const QString& new_text) {
- const int row_count = tree_view->model()->rowCount();
- const QString edit_filter_text = new_text.toLower();
- const QModelIndex root_index = item_model->invisibleRootItem()->index();
+ int folder_count = tree_view->model()->rowCount();
+ QString edit_filter_text = new_text.toLower();
+ QStandardItem* folder;
+ QStandardItem* child;
+ int childrenTotal = 0;
+ QModelIndex root_index = item_model->invisibleRootItem()->index();
// If the searchfield is empty every item is visible
// Otherwise the filter gets applied
if (edit_filter_text.isEmpty()) {
- for (int i = 0; i < row_count; ++i) {
- tree_view->setRowHidden(i, root_index, false);
+ 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) {
+ ++childrenTotal;
+ tree_view->setRowHidden(j, folder_index, false);
+ }
}
- search_field->setFilterResult(row_count, row_count);
+ search_field->setFilterResult(childrenTotal, childrenTotal);
} else {
int result_count = 0;
- for (int i = 0; i < row_count; ++i) {
- const QStandardItem* child_file = item_model->item(i, 0);
- const QString file_path =
- child_file->data(GameListItemPath::FullPathRole).toString().toLower();
- const QString file_title =
- child_file->data(GameListItemPath::TitleRole).toString().toLower();
- const QString file_program_id =
- child_file->data(GameListItemPath::ProgramIdRole).toString().toLower();
-
- // Only items which filename in combination with its title contains all words
- // that are in the searchfield will be visible in the gamelist
- // The search is case insensitive because of toLower()
- // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
- // multiple conversions of edit_filter_text for each game in the gamelist
- const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) +
- QLatin1Char{' '} + file_title;
- if (ContainsAllWords(file_name, edit_filter_text) ||
- (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
- tree_view->setRowHidden(i, root_index, false);
- ++result_count;
- } else {
- tree_view->setRowHidden(i, root_index, true);
+ 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) {
+ ++childrenTotal;
+ const QStandardItem* child = folder->child(j, 0);
+ 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
+ // that are in the searchfield will be visible in the gamelist
+ // The search is case insensitive because of toLower()
+ // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
+ // multiple conversions of edit_filter_text for each game in the gamelist
+ const QString file_name =
+ file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
+ file_title;
+ if (ContainsAllWords(file_name, edit_filter_text) ||
+ (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
+ tree_view->setRowHidden(j, folder_index, false);
+ ++result_count;
+ } 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_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::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
- // with signals/slots. In this case, QList falls under the umbrells of custom types.
+ // We must register all custom types with the Qt Automoc system so that we are able to use
+ // it with signals/slots. In this case, QList falls under the umbrells of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
layout->setContentsMargins(0, 0, 0, 0);
@@ -263,38 +345,67 @@ void GameList::clearFilter() {
search_field->clear();
}
-void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
+void GameList::AddDirEntry(GameListDir* 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::ValidateEntry(const QModelIndex& item) {
- // We don't care about the individual QStandardItem that was selected, but its row.
- 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())
- return;
-
- if (!QFileInfo::exists(file_path))
- return;
+void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
+ parent->appendRow(entry_items);
+}
- const QFileInfo file_info{file_path};
- if (file_info.isDir()) {
- const QDir dir{file_path};
- const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
- if (matching_main.size() == 1) {
- emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
+void GameList::ValidateEntry(const QModelIndex& item) {
+ auto selected = item.sibling(item.row(), 0);
+
+ switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
+ 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 (file_info.isDir()) {
+ const QDir dir{file_path};
+ const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
+ if (matching_main.size() == 1) {
+ emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
+ }
+ return;
}
- return;
+
+ // Users usually want to run a different game after closing one
+ search_field->clear();
+ emit GameChosen(file_path);
+ break;
}
+ case GameListItemType::AddDir:
+ emit AddDirectory();
+ break;
+ }
+}
- // Users usually want to run a diffrent game after closing one
- search_field->clear();
- emit GameChosen(file_path);
+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) {
+ emit ShowList(!isEmpty());
+
+ item_model->invisibleRootItem()->appendRow(new GameListAddDir());
+
// Clear out the old directories to watch for changes and add the new ones
auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) {
@@ -311,9 +422,16 @@ void GameList::DonePopulating(QStringList watch_list) {
QCoreApplication::processEvents();
}
tree_view->setEnabled(true);
- int rowCount = tree_view->model()->rowCount();
- search_field->setFilterResult(rowCount, rowCount);
- if (rowCount > 0) {
+ int folder_count = tree_view->model()->rowCount();
+ int childrenTotal = 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();
}
}
@@ -323,12 +441,26 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
if (!item.isValid())
return;
- int row = item_model->itemFromIndex(item)->row();
- 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();
-
+ auto selected = item.sibling(item.row(), 0);
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_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));
QAction* open_transferable_shader_cache =
@@ -344,19 +476,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
- connect(open_save_location, &QAction::triggered,
- [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); });
- connect(open_lfs_location, &QAction::triggered,
- [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); });
+ connect(open_save_location, &QAction::triggered, [this, program_id]() {
+ emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData);
+ });
+ connect(open_lfs_location, &QAction::triggered, [this, program_id]() {
+ emit OpenFolderRequested(program_id, GameListOpenTarget::ModData);
+ });
connect(open_transferable_shader_cache, &QAction::triggered,
- [&]() { emit OpenTransferableShaderCacheRequested(program_id); });
- connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); });
- connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); });
- connect(navigate_to_gamedb_entry, &QAction::triggered,
- [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); });
- connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); });
+ [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
+ connect(dump_romfs, &QAction::triggered,
+ [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); });
+ connect(copy_tid, &QAction::triggered,
+ [this, program_id]() { emit CopyTIDRequested(program_id); });
+ 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); });
+};
+
+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());
+ });
+}
- context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
+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() {
@@ -403,14 +602,7 @@ void GameList::LoadCompatibilityList() {
}
}
-void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
- 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;
- }
-
+void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) {
tree_view->setEnabled(false);
// 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
item_model->removeRows(0, item_model->rowCount());
+ search_field->clear();
emit ShouldCancelWorker();
- GameListWorker* worker =
- new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
+ GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);
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,
Qt::QueuedConnection);
- // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel
- // without delay.
+ // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
+ // cancel without delay.
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
Qt::DirectConnection);
@@ -471,10 +665,42 @@ const QStringList GameList::supported_file_extensions = {
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
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.");
- search_field->clear();
- PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ PopulateAsync(UISettings::values.game_dirs);
}
}
+
+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();
+}
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index f8f8bd6c5..a2b58aba5 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -19,10 +19,14 @@
#include <QWidget>
#include "common/common_types.h"
+#include "ui_settings.h"
#include "yuzu/compatibility_list.h"
class GameListWorker;
class GameListSearchField;
+template <typename>
+class QList;
+class GameListDir;
class GMainWindow;
namespace FileSys {
@@ -52,12 +56,14 @@ public:
FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
~GameList() override;
+ QString getLastFilterResultItem();
void clearFilter();
void setFilterFocus();
void setFilterVisible(bool visibility);
+ bool isEmpty();
void LoadCompatibilityList();
- void PopulateAsync(const QString& dir_path, bool deep_scan);
+ void PopulateAsync(QList<UISettings::GameDir>& game_dirs);
void SaveInterfaceLayout();
void LoadInterfaceLayout();
@@ -74,19 +80,29 @@ signals:
void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const std::string& file);
+ void OpenDirectory(QString directory);
+ void AddDirectory();
+ void ShowList(bool show);
private slots:
+ void onItemExpanded(const QModelIndex& item);
void onTextChanged(const QString& new_text);
void onFilterCloseClicked();
+ void onUpdateThemedIcons();
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 DonePopulating(QStringList watch_list);
- void PopupContextMenu(const QPoint& menu_location);
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;
FileSys::ManualContentProvider* provider;
GameListSearchField* search_field;
@@ -102,3 +118,25 @@ private:
};
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;
+};
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index ece534dd6..f5abb759d 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -10,6 +10,7 @@
#include <utility>
#include <QCoreApplication>
+#include <QFileInfo>
#include <QImage>
#include <QObject>
#include <QStandardItem>
@@ -22,6 +23,16 @@
#include "yuzu/uisettings.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)
* @param size The desired width and height of the default icon.
@@ -36,8 +47,13 @@ static QPixmap GetDefaultIcon(u32 size) {
class GameListItem : public QStandardItem {
public:
+ // used to access type from item index
+ static const int TypeRole = Qt::UserRole + 1;
+ static const int SortRole = Qt::UserRole + 2;
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 {
public:
- static const int FullPathRole = Qt::UserRole + 1;
- static const int TitleRole = Qt::UserRole + 2;
- static const int ProgramIdRole = Qt::UserRole + 3;
- static const int FileTypeRole = Qt::UserRole + 4;
+ static const int TitleRole = SortRole;
+ static const int FullPathRole = SortRole + 1;
+ static const int ProgramIdRole = SortRole + 2;
+ static const int FileTypeRole = SortRole + 3;
GameListItemPath() = default;
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
const QString& game_name, const QString& game_type, u64 program_id) {
+ setData(type(), TypeRole);
setData(game_path, FullPathRole);
setData(game_name, TitleRole);
setData(qulonglong(program_id), ProgramIdRole);
@@ -72,6 +89,10 @@ public:
setData(picture, Qt::DecorationRole);
}
+ int type() const override {
+ return static_cast<int>(GameListItemType::Game);
+ }
+
QVariant data(int role) const override {
if (role == Qt::DisplayRole) {
std::string filename;
@@ -103,9 +124,11 @@ public:
class GameListItemCompat : public GameListItem {
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
public:
- static const int CompatNumberRole = Qt::UserRole + 1;
+ static const int CompatNumberRole = SortRole;
GameListItemCompat() = default;
explicit GameListItemCompat(const QString& compatibility) {
+ setData(type(), TypeRole);
+
struct CompatStatus {
QString color;
const char* text;
@@ -135,6 +158,10 @@ public:
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) < other.data(CompatNumberRole);
}
@@ -146,12 +173,12 @@ public:
* human-readable string representation will be displayed to the user.
*/
class GameListItemSize : public GameListItem {
-
public:
- static const int SizeRole = Qt::UserRole + 1;
+ static const int SizeRole = SortRole;
GameListItemSize() = default;
explicit GameListItemSize(const qulonglong size_bytes) {
+ setData(type(), TypeRole);
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.
* 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 QHBoxLayout;
class QTreeView;
@@ -195,6 +287,9 @@ public:
void clear();
void setFocus();
+ int visible;
+ int total;
+
private:
class KeyReleaseEater : public QObject {
public:
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index 77f358630..8c6621c98 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -223,21 +223,38 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
} // Anonymous namespace
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
- FileSys::ManualContentProvider* provider, QString dir_path,
- bool deep_scan, const CompatibilityList& compatibility_list)
- : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan),
+ FileSys::ManualContentProvider* provider,
+ QList<UISettings::GameDir>& game_dirs,
+ const CompatibilityList& compatibility_list)
+ : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),
compatibility_list(compatibility_list) {}
GameListWorker::~GameListWorker() = default;
-void GameListWorker::AddTitlesToGameList() {
- const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>(
- Core::System::GetInstance().GetContentProvider());
- const auto installed_games = cache.ListEntriesFilterOrigin(
- std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program);
+void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
+ using namespace FileSys;
+
+ const auto& cache =
+ 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) {
- if (slot == FileSys::ContentProviderUnionSlot::FrontendManual)
+ if (slot == ContentProviderUnionSlot::FrontendManual)
continue;
const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
@@ -250,21 +267,22 @@ void GameListWorker::AddTitlesToGameList() {
u64 program_id = 0;
loader->ReadProgramId(program_id);
- const FileSys::PatchManager patch{program_id};
- const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control);
+ const PatchManager patch{program_id};
+ const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
if (control != nullptr)
GetMetadataFromControlNCA(patch, *control, icon, name);
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,
- unsigned int recursion) {
- const auto callback = [this, target, recursion](u64* num_entries_out,
- const std::string& directory,
- const std::string& virtual_name) -> bool {
+ unsigned int recursion, GameListDir* parent_dir) {
+ const auto callback = [this, target, recursion,
+ parent_dir](u64* num_entries_out, const std::string& directory,
+ const std::string& virtual_name) -> bool {
if (stop_processing) {
// Breaks the callback loop.
return false;
@@ -317,11 +335,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{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) {
watch_list.append(QString::fromStdString(physical_name));
- ScanFileSystem(target, physical_name, recursion - 1);
+ ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
}
return true;
@@ -332,12 +351,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
void GameListWorker::run() {
stop_processing = false;
- watch_list.append(dir_path);
- provider->ClearAllEntries();
- ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(),
- deep_scan ? 256 : 0);
- AddTitlesToGameList();
- ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0);
+
+ for (UISettings::GameDir& game_dir : game_dirs) {
+ if (game_dir.path == "INSTALLED") {
+ GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
+ emit DirEntryReady({game_list_dir});
+ 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);
}
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h
index 7c3074af9..46ec96516 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -33,9 +33,10 @@ class GameListWorker : public QObject, public QRunnable {
Q_OBJECT
public:
- GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
- FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan,
- const CompatibilityList& compatibility_list);
+ explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
+ FileSys::ManualContentProvider* provider,
+ QList<UISettings::GameDir>& game_dirs,
+ const CompatibilityList& compatibility_list);
~GameListWorker() override;
/// 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
* 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
- * with a list of folders that should be watched for changes as well.
+ * After the worker has traversed the game directory looking for entries, this signal is
+ * emitted with a list of folders that should be watched for changes as well.
*/
void Finished(QStringList watch_list);
private:
- void AddTitlesToGameList();
+ void AddTitlesToGameList(GameListDir* parent_dir);
enum class ScanTarget {
FillManualContentProvider,
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;
FileSys::ManualContentProvider* provider;
QStringList watch_list;
- QString dir_path;
- bool deep_scan;
const CompatibilityList& compatibility_list;
+ QList<UISettings::GameDir>& game_dirs;
std::atomic_bool stop_processing;
};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index ac57229d5..b2de9545b 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -216,8 +216,7 @@ GMainWindow::GMainWindow()
OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
game_list->LoadCompatibilityList();
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
// Show one-time "callout" messages to the user
ShowTelemetryCallout();
@@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() {
game_list = new GameList(vfs, provider.get(), this);
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->hide();
ui.horizontalLayout->addWidget(loading_screen);
@@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() {
void GMainWindow::ConnectWidgetEvents() {
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::OpenTransferableShaderCacheRequested, this,
&GMainWindow::OnTransferableShaderCacheOpenFile);
@@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&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,
&GMainWindow::OnGameListOpenPerGameProperties);
@@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
connect(ui.action_Install_File_NAND, &QAction::triggered, this,
&GMainWindow::OnMenuInstallToNAND);
- connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
- &GMainWindow::OnMenuSelectGameListRoot);
connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
[this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
@@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) {
// Update the GUI
if (ui.action_Single_Window_Mode->isChecked()) {
game_list->hide();
+ game_list_placeholder->hide();
}
status_bar_update_timer.start(2000);
@@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() {
render_window->hide();
loading_screen->hide();
loading_screen->Clear();
- game_list->show();
+ if (game_list->isEmpty())
+ game_list_placeholder->show();
+ else
+ game_list->show();
game_list->setFilterFocus();
UpdateWindowTitle();
@@ -1298,6 +1309,45 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
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) {
u64 title_id{};
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);
if (reload) {
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
}
config->Save();
@@ -1407,8 +1456,7 @@ void GMainWindow::OnMenuInstallToNAND() {
const auto success = [this]() {
QMessageBox::information(this, tr("Successfully Installed"),
tr("The file was successfully installed."));
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
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) {
const auto res = QMessageBox::information(
this, tr("Changing Emulated Directory"),
@@ -1559,8 +1599,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)
: FileUtil::UserPath::NANDDir,
dir_path.toStdString());
Service::FileSystem::CreateFactories(*vfs);
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
}
}
@@ -1724,11 +1763,11 @@ void GMainWindow::OnConfigure() {
if (UISettings::values.enable_discord_presence != old_discord_presence) {
SetDiscordEnabled(UISettings::values.enable_discord_presence);
}
+ emit UpdateThemedIcons();
const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
if (reload) {
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
}
config->Save();
@@ -1992,8 +2031,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
Service::FileSystem::CreateFactories(*vfs);
if (behavior == ReinitializeKeyBehavior::Warning) {
- game_list->PopulateAsync(UISettings::values.game_directory_path,
- UISettings::values.game_directory_deepscan);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
}
}
@@ -2158,7 +2196,6 @@ void GMainWindow::UpdateUITheme() {
}
QIcon::setThemeSearchPaths(theme_paths);
- emit UpdateThemedIcons();
}
void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 501608ddc..b7398b6c7 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -30,6 +30,7 @@ class ProfilerWidget;
class QLabel;
class WaitTreeWidget;
enum class GameListOpenTarget;
+class GameListPlaceholder;
namespace Core::Frontend {
struct SoftwareKeyboardParameters;
@@ -186,12 +187,13 @@ private slots:
void OnGameListCopyTID(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list);
+ void OnGameListOpenDirectory(QString path);
+ void OnGameListAddDirectory();
+ void OnGameListShowList(bool show);
void OnGameListOpenPerGameProperties(const std::string& file);
void OnMenuLoadFile();
void OnMenuLoadFolder();
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
void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target);
void OnMenuRecentFile();
@@ -223,6 +225,8 @@ private:
GameList* game_list;
LoadingScreen* loading_screen;
+ GameListPlaceholder* game_list_placeholder;
+
// Status bar elements
QLabel* message_label = nullptr;
QLabel* emu_speed_label = nullptr;
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index ffcabb495..a1ce3c0c3 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -62,7 +62,6 @@
<addaction name="action_Load_File"/>
<addaction name="action_Load_Folder"/>
<addaction name="separator"/>
- <addaction name="action_Select_Game_List_Root"/>
<addaction name="menu_recent_files"/>
<addaction name="separator"/>
<addaction name="action_Select_NAND_Directory"/>
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index a62cd6911..76348db69 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -8,6 +8,7 @@
#include <atomic>
#include <vector>
#include <QByteArray>
+#include <QMetaType>
#include <QString>
#include <QStringList>
#include "common/common_types.h"
@@ -25,6 +26,18 @@ struct Shortcut {
using Themes = std::array<std::pair<const char*, const char*>, 2>;
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 {
QByteArray geometry;
QByteArray state;
@@ -55,8 +68,9 @@ struct Values {
QString roms_path;
QString symbols_path;
QString screenshot_path;
- QString game_directory_path;
- bool game_directory_deepscan;
+ QString game_dir_deprecated;
+ bool game_dir_deprecated_deepscan;
+ QList<UISettings::GameDir> game_dirs;
QStringList recent_files;
QString theme;
@@ -84,3 +98,5 @@ struct Values {
extern Values values;
} // namespace UISettings
+
+Q_DECLARE_METATYPE(UISettings::GameDir*);