diff --git a/dist/license.md b/dist/license.md
new file mode 100644
index 000000000..b777ebb20
--- /dev/null
+++ b/dist/license.md
@@ -0,0 +1,31 @@
+The icons in this folder and its subfolders have the following licenses:
+
+Icon Name | License | Origin/Author
+--- | --- | ---
+qt_themes/default/icons/16x16/checked.png | Free for non-commercial use
+qt_themes/default/icons/16x16/failed.png | Free for non-commercial use
+qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
+qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/16x16/checked.png | Free for non-commercial use
+qt_themes/qdarkstyle/icons/16x16/failed.png | Free for non-commercial use
+qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
+qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+
+
\ No newline at end of file
diff --git a/dist/qt_themes/colorful/icons/16x16/lock.png b/dist/qt_themes/colorful/icons/16x16/lock.png
new file mode 100644
index 000000000..fd27069d8
Binary files /dev/null and b/dist/qt_themes/colorful/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/colorful/icons/256x256/plus_folder.png b/dist/qt_themes/colorful/icons/256x256/plus_folder.png
new file mode 100644
index 000000000..760fe6245
Binary files /dev/null and b/dist/qt_themes/colorful/icons/256x256/plus_folder.png differ
diff --git a/dist/qt_themes/colorful/icons/48x48/bad_folder.png b/dist/qt_themes/colorful/icons/48x48/bad_folder.png
new file mode 100644
index 000000000..a7ab7a1f6
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/bad_folder.png differ
diff --git a/dist/qt_themes/colorful/icons/48x48/chip.png b/dist/qt_themes/colorful/icons/48x48/chip.png
new file mode 100644
index 000000000..6fa158999
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/chip.png differ
diff --git a/dist/qt_themes/colorful/icons/48x48/folder.png b/dist/qt_themes/colorful/icons/48x48/folder.png
new file mode 100644
index 000000000..498de4c62
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/folder.png differ
diff --git a/dist/qt_themes/colorful/icons/48x48/plus.png b/dist/qt_themes/colorful/icons/48x48/plus.png
new file mode 100644
index 000000000..bc2c47c91
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/plus.png differ
diff --git a/dist/qt_themes/colorful/icons/48x48/sd_card.png b/dist/qt_themes/colorful/icons/48x48/sd_card.png
new file mode 100644
index 000000000..29be71a0d
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/colorful/icons/index.theme b/dist/qt_themes/colorful/icons/index.theme
new file mode 100644
index 000000000..b452aca16
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/index.theme
@@ -0,0 +1,14 @@
+[Icon Theme]
+Name=colorful
+Comment=Colorful theme
+Inherits=default
+Directories=16x16,48x48,256x256
+
+[16x16]
+Size=16
+
+[48x48]
+Size=48
+
+[256x256]
+Size=256
diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc
new file mode 100644
index 000000000..af2f3fd56
--- /dev/null
+++ b/dist/qt_themes/colorful/style.qrc
@@ -0,0 +1,15 @@
+
+
+ icons/index.theme
+ icons/16x16/lock.png
+ icons/48x48/bad_folder.png
+ icons/48x48/chip.png
+ icons/48x48/folder.png
+ icons/48x48/plus.png
+ icons/48x48/sd_card.png
+ icons/256x256/plus_folder.png
+
+
+ style.qss
+
+
diff --git a/dist/qt_themes/colorful/style.qss b/dist/qt_themes/colorful/style.qss
new file mode 100644
index 000000000..413fc81da
--- /dev/null
+++ b/dist/qt_themes/colorful/style.qss
@@ -0,0 +1,4 @@
+/*
+ This file is intentionally left blank.
+ We do not want to apply any stylesheet for colorful, only icons.
+*/
diff --git a/dist/qt_themes/colorful_dark/icons/16x16/lock.png b/dist/qt_themes/colorful_dark/icons/16x16/lock.png
new file mode 100644
index 000000000..32c505848
Binary files /dev/null and b/dist/qt_themes/colorful_dark/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/colorful_dark/icons/index.theme b/dist/qt_themes/colorful_dark/icons/index.theme
new file mode 100644
index 000000000..94d5ae8aa
--- /dev/null
+++ b/dist/qt_themes/colorful_dark/icons/index.theme
@@ -0,0 +1,8 @@
+[Icon Theme]
+Name=colorful_dark
+Comment=Colorful theme (Dark style)
+Inherits=default
+Directories=16x16
+
+[16x16]
+Size=16
diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc
new file mode 100644
index 000000000..27a6cc87d
--- /dev/null
+++ b/dist/qt_themes/colorful_dark/style.qrc
@@ -0,0 +1,57 @@
+
+
+ icons/index.theme
+ icons/16x16/lock.png
+ ../colorful/icons/48x48/bad_folder.png
+ ../colorful/icons/48x48/chip.png
+ ../colorful/icons/48x48/folder.png
+ ../colorful/icons/48x48/plus.png
+ ../colorful/icons/48x48/sd_card.png
+ ../colorful/icons/256x256/plus_folder.png
+
+
+
+ ../qdarkstyle/rc/up_arrow_disabled.png
+ ../qdarkstyle/rc/Hmovetoolbar.png
+ ../qdarkstyle/rc/stylesheet-branch-end.png
+ ../qdarkstyle/rc/branch_closed-on.png
+ ../qdarkstyle/rc/stylesheet-vline.png
+ ../qdarkstyle/rc/branch_closed.png
+ ../qdarkstyle/rc/branch_open-on.png
+ ../qdarkstyle/rc/transparent.png
+ ../qdarkstyle/rc/right_arrow_disabled.png
+ ../qdarkstyle/rc/sizegrip.png
+ ../qdarkstyle/rc/close.png
+ ../qdarkstyle/rc/close-hover.png
+ ../qdarkstyle/rc/close-pressed.png
+ ../qdarkstyle/rc/down_arrow.png
+ ../qdarkstyle/rc/Vmovetoolbar.png
+ ../qdarkstyle/rc/left_arrow.png
+ ../qdarkstyle/rc/stylesheet-branch-more.png
+ ../qdarkstyle/rc/up_arrow.png
+ ../qdarkstyle/rc/right_arrow.png
+ ../qdarkstyle/rc/left_arrow_disabled.png
+ ../qdarkstyle/rc/Hsepartoolbar.png
+ ../qdarkstyle/rc/branch_open.png
+ ../qdarkstyle/rc/Vsepartoolbar.png
+ ../qdarkstyle/rc/down_arrow_disabled.png
+ ../qdarkstyle/rc/undock.png
+ ../qdarkstyle/rc/checkbox_checked_disabled.png
+ ../qdarkstyle/rc/checkbox_checked_focus.png
+ ../qdarkstyle/rc/checkbox_checked.png
+ ../qdarkstyle/rc/checkbox_indeterminate.png
+ ../qdarkstyle/rc/checkbox_indeterminate_focus.png
+ ../qdarkstyle/rc/checkbox_unchecked_disabled.png
+ ../qdarkstyle/rc/checkbox_unchecked_focus.png
+ ../qdarkstyle/rc/checkbox_unchecked.png
+ ../qdarkstyle/rc/radio_checked_disabled.png
+ ../qdarkstyle/rc/radio_checked_focus.png
+ ../qdarkstyle/rc/radio_checked.png
+ ../qdarkstyle/rc/radio_unchecked_disabled.png
+ ../qdarkstyle/rc/radio_unchecked_focus.png
+ ../qdarkstyle/rc/radio_unchecked.png
+
+
+ ../qdarkstyle/style.qss
+
+
diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index 14a0cf6f9..d1a0ee1be 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -5,7 +5,21 @@
icons/16x16/checked.png
icons/16x16/failed.png
+
+ icons/16x16/lock.png
+
+ icons/48x48/bad_folder.png
+
+ icons/48x48/chip.png
+
+ icons/48x48/folder.png
+
+ icons/48x48/plus.png
+
+ icons/48x48/sd_card.png
icons/256x256/yuzu.png
+
+ icons/256x256/plus_folder.png
diff --git a/dist/qt_themes/default/icons/16x16/lock.png b/dist/qt_themes/default/icons/16x16/lock.png
new file mode 100644
index 000000000..496b58078
Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/default/icons/256x256/plus_folder.png b/dist/qt_themes/default/icons/256x256/plus_folder.png
new file mode 100644
index 000000000..ae4afccc7
Binary files /dev/null and b/dist/qt_themes/default/icons/256x256/plus_folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/bad_folder.png b/dist/qt_themes/default/icons/48x48/bad_folder.png
new file mode 100644
index 000000000..2527c1318
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/bad_folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/chip.png b/dist/qt_themes/default/icons/48x48/chip.png
new file mode 100644
index 000000000..3efdf301e
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/chip.png differ
diff --git a/dist/qt_themes/default/icons/48x48/folder.png b/dist/qt_themes/default/icons/48x48/folder.png
new file mode 100644
index 000000000..2e67d8b38
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/plus.png b/dist/qt_themes/default/icons/48x48/plus.png
new file mode 100644
index 000000000..dbc74687b
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/plus.png differ
diff --git a/dist/qt_themes/default/icons/48x48/sd_card.png b/dist/qt_themes/default/icons/48x48/sd_card.png
new file mode 100644
index 000000000..edacaeeb5
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/default/icons/index.theme b/dist/qt_themes/default/icons/index.theme
index ac67cb236..1edbe6408 100644
--- a/dist/qt_themes/default/icons/index.theme
+++ b/dist/qt_themes/default/icons/index.theme
@@ -1,10 +1,13 @@
[Icon Theme]
Name=default
Comment=default theme
-Directories=16x16,256x256
+Directories=16x16,48x48,256x256
[16x16]
Size=16
+
+[48x48]
+Size=48
[256x256]
Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/lock.png b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png
new file mode 100644
index 000000000..c750a39e8
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png b/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png
new file mode 100644
index 000000000..303f9a321
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png b/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png
new file mode 100644
index 000000000..4a9709623
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/chip.png b/dist/qt_themes/qdarkstyle/icons/48x48/chip.png
new file mode 100644
index 000000000..973fabd05
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/chip.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/folder.png b/dist/qt_themes/qdarkstyle/icons/48x48/folder.png
new file mode 100644
index 000000000..0f1e987d6
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/plus.png b/dist/qt_themes/qdarkstyle/icons/48x48/plus.png
new file mode 100644
index 000000000..16cc8b4f4
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/plus.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png
new file mode 100644
index 000000000..0291c6542
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/index.theme b/dist/qt_themes/qdarkstyle/icons/index.theme
index 558ece40b..d1e12f3ef 100644
--- a/dist/qt_themes/qdarkstyle/icons/index.theme
+++ b/dist/qt_themes/qdarkstyle/icons/index.theme
@@ -2,10 +2,13 @@
Name=qdarkstyle
Comment=dark theme
Inherits=default
-Directories=16x16,256x256
+Directories=16x16,48x48,256x256
[16x16]
Size=16
-
+
+[48x48]
+Size=48
+
[256x256]
Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index efbd0b9dc..c2c14c28a 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -1,6 +1,13 @@
icons/index.theme
+ icons/16x16/lock.png
+ icons/48x48/bad_folder.png
+ icons/48x48/chip.png
+ icons/48x48/folder.png
+ icons/48x48/plus.png
+ icons/48x48/sd_card.png
+ icons/256x256/plus_folder.png
rc/up_arrow_disabled.png
diff --git a/license.txt b/license.txt
index d511905c1..2b858f9a7 100644
--- a/license.txt
+++ b/license.txt
@@ -337,3 +337,19 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+
+
+The icons used in this project have the following licenses:
+
+Icon Name | License | Origin/Author
+--- | --- | ---
+checked.png | Free for non-commercial use
+failed.png | Free for non-commercial use
+lock.png | CC BY-ND 3.0 | https://icons8.com
+plus_folder.png | CC BY-ND 3.0 | https://icons8.com
+bad_folder.png | CC BY-ND 3.0 | https://icons8.com
+chip.png | CC BY-ND 3.0 | https://icons8.com
+folder.png | CC BY-ND 3.0 | https://icons8.com
+plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
+plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
+sd_card.png | CC BY-ND 3.0 | https://icons8.com
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 0456248ac..f594106bf 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -517,10 +517,37 @@ 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();
+ const 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("SDMC");
+ game_dir.expanded = true;
+ UISettings::values.game_dirs.append(game_dir);
+ game_dir.path = QStringLiteral("UserNAND");
+ UISettings::values.game_dirs.append(game_dir);
+ game_dir.path = QStringLiteral("SysNAND");
+ 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 +926,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[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
@@ -24,13 +24,6 @@
-
-
-
-
-
- Search sub-directories for games
-
-
-
-
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index d18b96519..d5fab2f1f 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(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() const {
+ QStandardItem* folder;
+ QStandardItem* child;
+ QString file_path;
+ const int folder_count = item_model->rowCount();
+ for (int i = 0; i < folder_count; ++i) {
+ folder = item_model->item(i, 0);
+ const QModelIndex folder_index = folder->index();
+ const int children_count = folder->rowCount();
+ for (int j = 0; j < children_count; ++j) {
+ 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,120 @@ 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) {
+ const auto type = item.data(GameListItem::TypeRole).value();
+ if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
+ type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir)
+ item.data(GameListDir::GameDirRole).value()->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();
+ const int folder_count = tree_view->model()->rowCount();
+ QString edit_filter_text = new_text.toLower();
+ QStandardItem* folder;
+ QStandardItem* child;
+ int children_total = 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);
+ const QModelIndex folder_index = folder->index();
+ const int children_count = folder->rowCount();
+ for (int j = 0; j < children_count; ++j) {
+ ++children_total;
+ tree_view->setRowHidden(j, folder_index, false);
+ }
}
- search_field->setFilterResult(row_count, row_count);
+ search_field->setFilterResult(children_total, children_total);
} 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();
+ for (int i = 0; i < folder_count; ++i) {
+ folder = item_model->item(i, 0);
+ const QModelIndex folder_index = folder->index();
+ const int children_count = folder->rowCount();
+ for (int j = 0; j < children_count; ++j) {
+ ++children_total;
+ const QStandardItem* child = folder->child(j, 0);
+ const 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(i, root_index, false);
- ++result_count;
- } else {
- tree_view->setRowHidden(i, root_index, true);
+ // 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, children_total);
}
- 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);
+
+ const int icon_size = std::min(static_cast(UISettings::values.icon_size), 64);
+ switch (child->data(GameListItem::TypeRole).value()) {
+ case GameListItemType::SdmcDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("sd_card"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::UserNandDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::SysNandDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::CustomDir: {
+ const UISettings::GameDir* game_dir =
+ child->data(GameListDir::GameDirRole).value();
+ const 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;
}
}
}
@@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
tree_view->setSortingEnabled(true);
tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
- tree_view->setUniformRowHeights(true);
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
@@ -230,12 +315,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");
layout->setContentsMargins(0, 0, 0, 0);
@@ -263,38 +352,68 @@ void GameList::clearFilter() {
search_field->clear();
}
-void GameList::AddEntry(const QList& 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()->expanded);
+}
+
+void GameList::AddEntry(const QList& entry_items, GameListDir* parent) {
+ parent->appendRow(entry_items);
}
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();
+ const auto selected = item.sibling(item.row(), 0);
- if (file_path.isEmpty())
- return;
+ switch (selected.data(GameListItem::TypeRole).value()) {
+ case GameListItemType::Game: {
+ const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
+ if (file_path.isEmpty())
+ return;
+ const QFileInfo file_info(file_path);
+ if (!file_info.exists())
+ return;
- if (!QFileInfo::exists(file_path))
- return;
-
- 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]);
+ 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 diffrent game after closing one
- search_field->clear();
- emit GameChosen(file_path);
+ // 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;
+ }
+}
+
+bool GameList::isEmpty() const {
+ for (int i = 0; i < item_model->rowCount(); i++) {
+ const QStandardItem* child = item_model->invisibleRootItem()->child(i);
+ const auto type = static_cast(child->type());
+ if (!child->hasChildren() &&
+ (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
+ type == GameListItemType::SysNandDir)) {
+ item_model->invisibleRootItem()->removeRow(child->row());
+ i--;
+ };
+ }
+ return !item_model->invisibleRootItem()->hasChildren();
}
void GameList::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 +430,13 @@ 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) {
+ const int folder_count = tree_view->model()->rowCount();
+ int children_total = 0;
+ for (int i = 0; i < folder_count; ++i) {
+ children_total += item_model->item(i, 0)->rowCount();
+ }
+ search_field->setFilterResult(children_total, children_total);
+ if (children_total > 0) {
search_field->setFocus();
}
}
@@ -323,12 +446,27 @@ 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();
-
+ const auto selected = item.sibling(item.row(), 0);
QMenu context_menu;
+ switch (selected.data(GameListItem::TypeRole).value()) {
+ 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::SdmcDir:
+ case GameListItemType::UserNandDir:
+ case GameListItemType::SysNandDir:
+ 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 +482,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); });
+};
- 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();
+
+ 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();
+
+ 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"));
+
+ const 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
+ std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
+ UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
+ *selected.sibling(row - 1, 0)
+ .data(GameListDir::GameDirRole)
+ .value())]);
+ // move the treeview items
+ QList 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
+ std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
+ UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
+ *selected.sibling(row + 1, 0)
+ .data(GameListDir::GameDirRole)
+ .value())]);
+ // move the treeview items
+ const QList 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 +608,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(QVector& game_dirs) {
tree_view->setEnabled(false);
// Update the columns in case UISettings has changed
@@ -433,17 +631,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 +671,40 @@ 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} {
+ connect(parent, &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..878d94413 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -16,13 +17,16 @@
#include
#include
#include
+#include
#include
#include "common/common_types.h"
+#include "uisettings.h"
#include "yuzu/compatibility_list.h"
class GameListWorker;
class GameListSearchField;
+class GameListDir;
class GMainWindow;
namespace FileSys {
@@ -52,12 +56,14 @@ public:
FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
~GameList() override;
+ QString getLastFilterResultItem() const;
void clearFilter();
void setFilterFocus();
void setFilterVisible(bool visibility);
+ bool isEmpty() const;
void LoadCompatibilityList();
- void PopulateAsync(const QString& dir_path, bool deep_scan);
+ void PopulateAsync(QVector& 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(const 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& entry_items);
+ void AddDirEntry(GameListDir* entry_items);
+ void AddEntry(const QList& 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 vfs;
FileSys::ManualContentProvider* provider;
GameListSearchField* search_field;
@@ -102,3 +118,24 @@ 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:
+ 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..a8d888fee 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -10,6 +10,7 @@
#include
#include
+#include
#include
#include
#include
@@ -22,6 +23,17 @@
#include "yuzu/uisettings.h"
#include "yuzu/util/util.h"
+enum class GameListItemType {
+ Game = QStandardItem::UserType + 1,
+ CustomDir = QStandardItem::UserType + 2,
+ SdmcDir = QStandardItem::UserType + 3,
+ UserNandDir = QStandardItem::UserType + 4,
+ SysNandDir = QStandardItem::UserType + 5,
+ AddDir = QStandardItem::UserType + 6
+};
+
+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 +48,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 +65,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& 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 +90,10 @@ public:
setData(picture, Qt::DecorationRole);
}
+ int type() const override {
+ return static_cast(GameListItemType::Game);
+ }
+
QVariant data(int role) const override {
if (role == Qt::DisplayRole) {
std::string filename;
@@ -103,9 +125,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 +159,10 @@ public:
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
}
+ int type() const override {
+ return static_cast(GameListItemType::Game);
+ }
+
bool operator<(const QStandardItem& other) const override {
return data(CompatNumberRole) < other.data(CompatNumberRole);
}
@@ -146,12 +174,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 +195,10 @@ public:
}
}
+ int type() const override {
+ return static_cast(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 +209,82 @@ 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);
+
+ const int icon_size = std::min(static_cast(UISettings::values.icon_size), 64);
+ switch (dir_type) {
+ case GameListItemType::SdmcDir:
+ setData(
+ QIcon::fromTheme(QStringLiteral("sd_card"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
+ break;
+ case GameListItemType::UserNandDir:
+ setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
+ break;
+ case GameListItemType::SysNandDir:
+ setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(QObject::tr("System Titles"), Qt::DisplayRole);
+ break;
+ case GameListItemType::CustomDir:
+ const QString icon_name = QFileInfo::exists(game_dir->path)
+ ? QStringLiteral("folder")
+ : QStringLiteral("bad_folder");
+ setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
+ icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(game_dir->path, Qt::DisplayRole);
+ break;
+ };
+ };
+
+ int type() const override {
+ return static_cast(dir_type);
+ }
+
+private:
+ GameListItemType dir_type;
+};
+
+class GameListAddDir : public GameListItem {
+public:
+ explicit GameListAddDir() {
+ setData(type(), TypeRole);
+
+ const int icon_size = std::min(static_cast(UISettings::values.icon_size), 64);
+ setData(QIcon::fromTheme(QStringLiteral("plus"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
+ }
+
+ int type() const override {
+ return static_cast(GameListItemType::AddDir);
+ }
+};
+
class GameList;
class QHBoxLayout;
class QTreeView;
@@ -208,6 +316,9 @@ private:
// EventFilter in order to process systemkeys while editing the searchfield
bool eventFilter(QObject* obj, QEvent* event) override;
};
+ int visible;
+ int total;
+
QHBoxLayout* layout_filter = nullptr;
QTreeView* tree_view = nullptr;
QLabel* label_filter = nullptr;
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index 77f358630..fd21a9761 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -223,21 +223,37 @@ QList 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,
+ QVector& 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(
- 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(Core::System::GetInstance().GetContentProvider());
+
+ std::vector> installed_games;
+ installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
+ ContentRecordType::Program);
+
+ if (parent_dir->type() == static_cast(GameListItemType::SdmcDir)) {
+ installed_games = cache.ListEntriesFilterOrigin(
+ ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
+ } else if (parent_dir->type() == static_cast(GameListItemType::UserNandDir)) {
+ installed_games = cache.ListEntriesFilterOrigin(
+ ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
+ } else if (parent_dir->type() == static_cast(GameListItemType::SysNandDir)) {
+ 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 +266,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 +334,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 +350,32 @@ 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 == QStringLiteral("SDMC")) {
+ auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
+ emit DirEntryReady({game_list_dir});
+ AddTitlesToGameList(game_list_dir);
+ } else if (game_dir.path == QStringLiteral("UserNAND")) {
+ auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
+ emit DirEntryReady({game_list_dir});
+ AddTitlesToGameList(game_list_dir);
+ } else if (game_dir.path == QStringLiteral("SysNAND")) {
+ auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
+ emit DirEntryReady({game_list_dir});
+ AddTitlesToGameList(game_list_dir);
+ } else {
+ watch_list.append(game_dir.path);
+ auto* const 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..6e52fca89 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include "common/common_types.h"
#include "yuzu/compatibility_list.h"
@@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable {
Q_OBJECT
public:
- GameListWorker(std::shared_ptr vfs,
- FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan,
- const CompatibilityList& compatibility_list);
+ explicit GameListWorker(std::shared_ptr vfs,
+ FileSys::ManualContentProvider* provider,
+ QVector& game_dirs,
+ const CompatibilityList& compatibility_list);
~GameListWorker() override;
/// Starts the processing of directory tree information.
@@ -48,31 +50,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 entry_items);
+ void DirEntryReady(GameListDir* entry_items);
+ void EntryReady(QList 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 vfs;
FileSys::ManualContentProvider* provider;
QStringList watch_list;
- QString dir_path;
- bool deep_scan;
const CompatibilityList& compatibility_list;
+ QVector& game_dirs;
std::atomic_bool stop_processing;
};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index ac57229d5..6d249cb3e 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,47 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
}
+void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
+ QString path;
+ if (directory == QStringLiteral("SDMC")) {
+ path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
+ "Nintendo/Contents/registered");
+ } else if (directory == QStringLiteral("UserNAND")) {
+ path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
+ "user/Contents/registered");
+ } else if (directory == QStringLiteral("SysNAND")) {
+ path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
+ "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() {
+ const 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 +1368,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 +1458,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 +1583,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 +1601,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 +1765,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 +2033,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 +2198,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..7d16188cb 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(const QString& directory);
+ 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 @@
-
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index a62cd6911..c57290006 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -8,8 +8,10 @@
#include
#include
#include
+#include
#include
#include
+#include
#include "common/common_types.h"
namespace UISettings {
@@ -25,6 +27,18 @@ struct Shortcut {
using Themes = std::array, 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 +69,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;
+ QVector game_dirs;
QStringList recent_files;
QString theme;
@@ -84,3 +99,5 @@ struct Values {
extern Values values;
} // namespace UISettings
+
+Q_DECLARE_METATYPE(UISettings::GameDir*);