summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dist/license.md31
-rw-r--r--dist/qt_themes/colorful/icons/16x16/lock.pngbin0 -> 330 bytes
-rw-r--r--dist/qt_themes/colorful/icons/256x256/plus_folder.pngbin0 -> 4643 bytes
-rw-r--r--dist/qt_themes/colorful/icons/48x48/bad_folder.pngbin0 -> 15494 bytes
-rw-r--r--dist/qt_themes/colorful/icons/48x48/chip.pngbin0 -> 582 bytes
-rw-r--r--dist/qt_themes/colorful/icons/48x48/folder.pngbin0 -> 460 bytes
-rw-r--r--dist/qt_themes/colorful/icons/48x48/plus.pngbin0 -> 496 bytes
-rw-r--r--dist/qt_themes/colorful/icons/48x48/sd_card.pngbin0 -> 680 bytes
-rw-r--r--dist/qt_themes/colorful/icons/index.theme14
-rw-r--r--dist/qt_themes/colorful/style.qrc15
-rw-r--r--dist/qt_themes/colorful/style.qss4
-rw-r--r--dist/qt_themes/colorful_dark/icons/16x16/lock.pngbin0 -> 401 bytes
-rw-r--r--dist/qt_themes/colorful_dark/icons/index.theme8
-rw-r--r--dist/qt_themes/colorful_dark/style.qrc57
-rw-r--r--dist/qt_themes/default/default.qrc14
-rw-r--r--dist/qt_themes/default/icons/16x16/lock.pngbin0 -> 279 bytes
-rw-r--r--dist/qt_themes/default/icons/256x256/plus_folder.pngbin0 -> 3135 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/bad_folder.pngbin0 -> 1088 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/chip.pngbin0 -> 15070 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/folder.pngbin0 -> 410 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/plus.pngbin0 -> 316 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/sd_card.pngbin0 -> 614 bytes
-rw-r--r--dist/qt_themes/default/icons/index.theme5
-rw-r--r--dist/qt_themes/qdarkstyle/icons/16x16/lock.pngbin0 -> 304 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.pngbin0 -> 3438 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.pngbin0 -> 1098 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/chip.pngbin0 -> 15120 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/folder.pngbin0 -> 542 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/plus.pngbin0 -> 339 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/sd_card.pngbin0 -> 676 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/index.theme7
-rw-r--r--dist/qt_themes/qdarkstyle/style.qrc7
-rw-r--r--license.txt16
-rw-r--r--src/yuzu/configuration/config.cpp44
-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.cpp436
-rw-r--r--src/yuzu/game_list.h43
-rw-r--r--src/yuzu/game_list_p.h127
-rw-r--r--src/yuzu/game_list_worker.cpp86
-rw-r--r--src/yuzu/game_list_worker.h26
-rw-r--r--src/yuzu/main.cpp87
-rw-r--r--src/yuzu/main.h8
-rw-r--r--src/yuzu/main.ui1
-rw-r--r--src/yuzu/uisettings.h21
45 files changed, 870 insertions, 199 deletions
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 @@
1The icons in this folder and its subfolders have the following licenses:
2
3Icon Name | License | Origin/Author
4--- | --- | ---
5qt_themes/default/icons/16x16/checked.png | Free for non-commercial use
6qt_themes/default/icons/16x16/failed.png | Free for non-commercial use
7qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
8qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
9qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
10qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
11qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
12qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
13qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
14qt_themes/qdarkstyle/icons/16x16/checked.png | Free for non-commercial use
15qt_themes/qdarkstyle/icons/16x16/failed.png | Free for non-commercial use
16qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
17qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
18qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
19qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
20qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
21qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
22qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
23qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
24qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
25qt_themes/colorful/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
26qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
27qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
28qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
29qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
30
31<!-- TODO: Add the license of the yuzu icon --> \ 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/16x16/lock.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/256x256/plus_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/48x48/bad_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/48x48/chip.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/48x48/folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/48x48/plus.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/48x48/sd_card.png
Binary files 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 @@
1[Icon Theme]
2Name=colorful
3Comment=Colorful theme
4Inherits=default
5Directories=16x16,48x48,256x256
6
7[16x16]
8Size=16
9
10[48x48]
11Size=48
12
13[256x256]
14Size=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 @@
1<RCC>
2 <qresource prefix="icons/colorful">
3 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
6 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
7 <file alias="48x48/folder.png">icons/48x48/folder.png</file>
8 <file alias="48x48/plus.png">icons/48x48/plus.png</file>
9 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
10 <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
11 </qresource>
12 <qresource prefix="colorful">
13 <file>style.qss</file>
14 </qresource>
15</RCC>
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 @@
1/*
2 This file is intentionally left blank.
3 We do not want to apply any stylesheet for colorful, only icons.
4*/
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
--- /dev/null
+++ b/dist/qt_themes/colorful_dark/icons/16x16/lock.png
Binary files 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 @@
1[Icon Theme]
2Name=colorful_dark
3Comment=Colorful theme (Dark style)
4Inherits=default
5Directories=16x16
6
7[16x16]
8Size=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 @@
1<RCC>
2 <qresource prefix="icons/colorful_dark">
3 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
6 <file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
7 <file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
8 <file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file>
9 <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>
10 <file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file>
11 </qresource>
12
13 <qresource prefix="qss_icons">
14 <file alias="rc/up_arrow_disabled.png">../qdarkstyle/rc/up_arrow_disabled.png</file>
15 <file alias="rc/Hmovetoolbar.png">../qdarkstyle/rc/Hmovetoolbar.png</file>
16 <file alias="rc/stylesheet-branch-end.png">../qdarkstyle/rc/stylesheet-branch-end.png</file>
17 <file alias="rc/branch_closed-on.png">../qdarkstyle/rc/branch_closed-on.png</file>
18 <file alias="rc/stylesheet-vline.png">../qdarkstyle/rc/stylesheet-vline.png</file>
19 <file alias="rc/branch_closed.png">../qdarkstyle/rc/branch_closed.png</file>
20 <file alias="rc/branch_open-on.png">../qdarkstyle/rc/branch_open-on.png</file>
21 <file alias="rc/transparent.png">../qdarkstyle/rc/transparent.png</file>
22 <file alias="rc/right_arrow_disabled.png">../qdarkstyle/rc/right_arrow_disabled.png</file>
23 <file alias="rc/sizegrip.png">../qdarkstyle/rc/sizegrip.png</file>
24 <file alias="rc/close.png">../qdarkstyle/rc/close.png</file>
25 <file alias="rc/close-hover.png">../qdarkstyle/rc/close-hover.png</file>
26 <file alias="rc/close-pressed.png">../qdarkstyle/rc/close-pressed.png</file>
27 <file alias="rc/down_arrow.png">../qdarkstyle/rc/down_arrow.png</file>
28 <file alias="rc/Vmovetoolbar.png">../qdarkstyle/rc/Vmovetoolbar.png</file>
29 <file alias="rc/left_arrow.png">../qdarkstyle/rc/left_arrow.png</file>
30 <file alias="rc/stylesheet-branch-more.png">../qdarkstyle/rc/stylesheet-branch-more.png</file>
31 <file alias="rc/up_arrow.png">../qdarkstyle/rc/up_arrow.png</file>
32 <file alias="rc/right_arrow.png">../qdarkstyle/rc/right_arrow.png</file>
33 <file alias="rc/left_arrow_disabled.png">../qdarkstyle/rc/left_arrow_disabled.png</file>
34 <file alias="rc/Hsepartoolbar.png">../qdarkstyle/rc/Hsepartoolbar.png</file>
35 <file alias="rc/branch_open.png">../qdarkstyle/rc/branch_open.png</file>
36 <file alias="rc/Vsepartoolbar.png">../qdarkstyle/rc/Vsepartoolbar.png</file>
37 <file alias="rc/down_arrow_disabled.png">../qdarkstyle/rc/down_arrow_disabled.png</file>
38 <file alias="rc/undock.png">../qdarkstyle/rc/undock.png</file>
39 <file alias="rc/checkbox_checked_disabled.png">../qdarkstyle/rc/checkbox_checked_disabled.png</file>
40 <file alias="rc/checkbox_checked_focus.png">../qdarkstyle/rc/checkbox_checked_focus.png</file>
41 <file alias="rc/checkbox_checked.png">../qdarkstyle/rc/checkbox_checked.png</file>
42 <file alias="rc/checkbox_indeterminate.png">../qdarkstyle/rc/checkbox_indeterminate.png</file>
43 <file alias="rc/checkbox_indeterminate_focus.png">../qdarkstyle/rc/checkbox_indeterminate_focus.png</file>
44 <file alias="rc/checkbox_unchecked_disabled.png">../qdarkstyle/rc/checkbox_unchecked_disabled.png</file>
45 <file alias="rc/checkbox_unchecked_focus.png">../qdarkstyle/rc/checkbox_unchecked_focus.png</file>
46 <file alias="rc/checkbox_unchecked.png">../qdarkstyle/rc/checkbox_unchecked.png</file>
47 <file alias="rc/radio_checked_disabled.png">../qdarkstyle/rc/radio_checked_disabled.png</file>
48 <file alias="rc/radio_checked_focus.png">../qdarkstyle/rc/radio_checked_focus.png</file>
49 <file alias="rc/radio_checked.png">../qdarkstyle/rc/radio_checked.png</file>
50 <file alias="rc/radio_unchecked_disabled.png">../qdarkstyle/rc/radio_unchecked_disabled.png</file>
51 <file alias="rc/radio_unchecked_focus.png">../qdarkstyle/rc/radio_unchecked_focus.png</file>
52 <file alias="rc/radio_unchecked.png">../qdarkstyle/rc/radio_unchecked.png</file>
53 </qresource>
54 <qresource prefix="colorful_dark">
55 <file alias="style.qss">../qdarkstyle/style.qss</file>
56 </qresource>
57</RCC>
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 @@
5 <file alias="16x16/checked.png">icons/16x16/checked.png</file> 5 <file alias="16x16/checked.png">icons/16x16/checked.png</file>
6 6
7 <file alias="16x16/failed.png">icons/16x16/failed.png</file> 7 <file alias="16x16/failed.png">icons/16x16/failed.png</file>
8
9 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
10
11 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
12
13 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
14
15 <file alias="48x48/folder.png">icons/48x48/folder.png</file>
16
17 <file alias="48x48/plus.png">icons/48x48/plus.png</file>
18
19 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
8 20
9 <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file> 21 <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file>
22
23 <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
10 </qresource> 24 </qresource>
11</RCC> 25</RCC>
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
--- /dev/null
+++ b/dist/qt_themes/default/icons/16x16/lock.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/256x256/plus_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/bad_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/chip.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/plus.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/sd_card.png
Binary files 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 @@
1[Icon Theme] 1[Icon Theme]
2Name=default 2Name=default
3Comment=default theme 3Comment=default theme
4Directories=16x16,256x256 4Directories=16x16,48x48,256x256
5 5
6[16x16] 6[16x16]
7Size=16 7Size=16
8
9[48x48]
10Size=48
8 11
9[256x256] 12[256x256]
10Size=256 \ No newline at end of file 13Size=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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/chip.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/folder.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/plus.png
Binary files 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
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png
Binary files 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 @@
2Name=qdarkstyle 2Name=qdarkstyle
3Comment=dark theme 3Comment=dark theme
4Inherits=default 4Inherits=default
5Directories=16x16,256x256 5Directories=16x16,48x48,256x256
6 6
7[16x16] 7[16x16]
8Size=16 8Size=16
9 9
10[48x48]
11Size=48
12
10[256x256] 13[256x256]
11Size=256 \ No newline at end of file 14Size=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 @@
1<RCC> 1<RCC>
2 <qresource prefix="icons/qdarkstyle"> 2 <qresource prefix="icons/qdarkstyle">
3 <file alias="index.theme">icons/index.theme</file> 3 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
6 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
7 <file alias="48x48/folder.png">icons/48x48/folder.png</file>
8 <file alias="48x48/plus.png">icons/48x48/plus.png</file>
9 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
10 <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
4 </qresource> 11 </qresource>
5 <qresource prefix="qss_icons"> 12 <qresource prefix="qss_icons">
6 <file>rc/up_arrow_disabled.png</file> 13 <file>rc/up_arrow_disabled.png</file>
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
337consider it more useful to permit linking proprietary applications with the 337consider it more useful to permit linking proprietary applications with the
338library. If this is what you want to do, use the GNU Lesser General 338library. If this is what you want to do, use the GNU Lesser General
339Public License instead of this License. 339Public License instead of this License.
340
341
342The icons used in this project have the following licenses:
343
344Icon Name | License | Origin/Author
345--- | --- | ---
346checked.png | Free for non-commercial use
347failed.png | Free for non-commercial use
348lock.png | CC BY-ND 3.0 | https://icons8.com
349plus_folder.png | CC BY-ND 3.0 | https://icons8.com
350bad_folder.png | CC BY-ND 3.0 | https://icons8.com
351chip.png | CC BY-ND 3.0 | https://icons8.com
352folder.png | CC BY-ND 3.0 | https://icons8.com
353plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
354plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
355sd_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() {
517 UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString(); 517 UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString();
518 UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString(); 518 UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString();
519 UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); 519 UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString();
520 UISettings::values.game_directory_path = 520 UISettings::values.game_dir_deprecated =
521 ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); 521 ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString();
522 UISettings::values.game_directory_deepscan = 522 UISettings::values.game_dir_deprecated_deepscan =
523 ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); 523 ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool();
524 const int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs"));
525 for (int i = 0; i < gamedirs_size; ++i) {
526 qt_config->setArrayIndex(i);
527 UISettings::GameDir game_dir;
528 game_dir.path = ReadSetting(QStringLiteral("path")).toString();
529 game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool();
530 game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool();
531 UISettings::values.game_dirs.append(game_dir);
532 }
533 qt_config->endArray();
534 // create NAND and SD card directories if empty, these are not removable through the UI,
535 // also carries over old game list settings if present
536 if (UISettings::values.game_dirs.isEmpty()) {
537 UISettings::GameDir game_dir;
538 game_dir.path = QStringLiteral("SDMC");
539 game_dir.expanded = true;
540 UISettings::values.game_dirs.append(game_dir);
541 game_dir.path = QStringLiteral("UserNAND");
542 UISettings::values.game_dirs.append(game_dir);
543 game_dir.path = QStringLiteral("SysNAND");
544 UISettings::values.game_dirs.append(game_dir);
545 if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) {
546 game_dir.path = UISettings::values.game_dir_deprecated;
547 game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan;
548 UISettings::values.game_dirs.append(game_dir);
549 }
550 }
524 UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); 551 UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
525 552
526 qt_config->endGroup(); 553 qt_config->endGroup();
@@ -899,10 +926,15 @@ void Config::SavePathValues() {
899 WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); 926 WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);
900 WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); 927 WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);
901 WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); 928 WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path);
902 WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, 929 qt_config->beginWriteArray(QStringLiteral("gamedirs"));
903 QStringLiteral(".")); 930 for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
904 WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, 931 qt_config->setArrayIndex(i);
905 false); 932 const auto& game_dir = UISettings::values.game_dirs[i];
933 WriteSetting(QStringLiteral("path"), game_dir.path);
934 WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false);
935 WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
936 }
937 qt_config->endArray();
906 WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); 938 WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);
907 939
908 qt_config->endGroup(); 940 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)
19 } 19 }
20 20
21 SetConfiguration(); 21 SetConfiguration();
22
23 connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this,
24 [] { UISettings::values.is_game_list_reload_pending.exchange(true); });
25} 22}
26 23
27ConfigureGeneral::~ConfigureGeneral() = default; 24ConfigureGeneral::~ConfigureGeneral() = default;
28 25
29void ConfigureGeneral::SetConfiguration() { 26void ConfigureGeneral::SetConfiguration() {
30 ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan);
31 ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); 27 ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
32 ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot); 28 ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot);
33 ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); 29 ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
34} 30}
35 31
36void ConfigureGeneral::ApplyConfiguration() { 32void ConfigureGeneral::ApplyConfiguration() {
37 UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked();
38 UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); 33 UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
39 UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked(); 34 UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked();
40 UISettings::values.theme = 35 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 @@
25 <item> 25 <item>
26 <layout class="QVBoxLayout" name="GeneralVerticalLayout"> 26 <layout class="QVBoxLayout" name="GeneralVerticalLayout">
27 <item> 27 <item>
28 <widget class="QCheckBox" name="toggle_deepscan">
29 <property name="text">
30 <string>Search sub-directories for games</string>
31 </property>
32 </widget>
33 </item>
34 <item>
35 <widget class="QCheckBox" name="toggle_check_exit"> 28 <widget class="QCheckBox" name="toggle_check_exit">
36 <property name="text"> 29 <property name="text">
37 <string>Confirm exit while emulation is running</string> 30 <string>Confirm exit while emulation is running</string>
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
34 return QObject::eventFilter(obj, event); 34 return QObject::eventFilter(obj, event);
35 35
36 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); 36 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
37 int rowCount = gamelist->tree_view->model()->rowCount();
38 QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower(); 37 QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
39 38
40 // If the searchfield's text hasn't changed special function keys get checked 39 // 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
56 // If there is only one result launch this game 55 // If there is only one result launch this game
57 case Qt::Key_Return: 56 case Qt::Key_Return:
58 case Qt::Key_Enter: { 57 case Qt::Key_Enter: {
59 QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view); 58 if (gamelist->search_field->visible == 1) {
60 QModelIndex root_index = item_model->invisibleRootItem()->index(); 59 QString file_path = gamelist->getLastFilterResultItem();
61 QStandardItem* child_file; 60
62 QString file_path;
63 int resultCount = 0;
64 for (int i = 0; i < rowCount; ++i) {
65 if (!gamelist->tree_view->isRowHidden(i, root_index)) {
66 ++resultCount;
67 child_file = gamelist->item_model->item(i, 0);
68 file_path = child_file->data(GameListItemPath::FullPathRole).toString();
69 }
70 }
71 if (resultCount == 1) {
72 // To avoid loading error dialog loops while confirming them using enter 61 // To avoid loading error dialog loops while confirming them using enter
73 // Also users usually want to run a different game after closing one 62 // Also users usually want to run a different game after closing one
74 gamelist->search_field->edit_filter->clear(); 63 gamelist->search_field->edit_filter->clear();
@@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
88} 77}
89 78
90void GameListSearchField::setFilterResult(int visible, int total) { 79void GameListSearchField::setFilterResult(int visible, int total) {
80 this->visible = visible;
81 this->total = total;
82
91 label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); 83 label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
92} 84}
93 85
86QString GameList::getLastFilterResultItem() const {
87 QStandardItem* folder;
88 QStandardItem* child;
89 QString file_path;
90 const int folder_count = item_model->rowCount();
91 for (int i = 0; i < folder_count; ++i) {
92 folder = item_model->item(i, 0);
93 const QModelIndex folder_index = folder->index();
94 const int children_count = folder->rowCount();
95 for (int j = 0; j < children_count; ++j) {
96 if (!tree_view->isRowHidden(j, folder_index)) {
97 child = folder->child(j, 0);
98 file_path = child->data(GameListItemPath::FullPathRole).toString();
99 }
100 }
101 }
102 return file_path;
103}
104
94void GameListSearchField::clear() { 105void GameListSearchField::clear() {
95 edit_filter->clear(); 106 edit_filter->clear();
96} 107}
@@ -147,45 +158,120 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)
147 [&haystack](const QString& s) { return haystack.contains(s); }); 158 [&haystack](const QString& s) { return haystack.contains(s); });
148} 159}
149 160
161// Syncs the expanded state of Game Directories with settings to persist across sessions
162void GameList::onItemExpanded(const QModelIndex& item) {
163 const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>();
164 if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
165 type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir)
166 item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded =
167 tree_view->isExpanded(item);
168}
169
150// Event in order to filter the gamelist after editing the searchfield 170// Event in order to filter the gamelist after editing the searchfield
151void GameList::onTextChanged(const QString& new_text) { 171void GameList::onTextChanged(const QString& new_text) {
152 const int row_count = tree_view->model()->rowCount(); 172 const int folder_count = tree_view->model()->rowCount();
153 const QString edit_filter_text = new_text.toLower(); 173 QString edit_filter_text = new_text.toLower();
154 const QModelIndex root_index = item_model->invisibleRootItem()->index(); 174 QStandardItem* folder;
175 QStandardItem* child;
176 int children_total = 0;
177 QModelIndex root_index = item_model->invisibleRootItem()->index();
155 178
156 // If the searchfield is empty every item is visible 179 // If the searchfield is empty every item is visible
157 // Otherwise the filter gets applied 180 // Otherwise the filter gets applied
158 if (edit_filter_text.isEmpty()) { 181 if (edit_filter_text.isEmpty()) {
159 for (int i = 0; i < row_count; ++i) { 182 for (int i = 0; i < folder_count; ++i) {
160 tree_view->setRowHidden(i, root_index, false); 183 folder = item_model->item(i, 0);
184 const QModelIndex folder_index = folder->index();
185 const int children_count = folder->rowCount();
186 for (int j = 0; j < children_count; ++j) {
187 ++children_total;
188 tree_view->setRowHidden(j, folder_index, false);
189 }
161 } 190 }
162 search_field->setFilterResult(row_count, row_count); 191 search_field->setFilterResult(children_total, children_total);
163 } else { 192 } else {
164 int result_count = 0; 193 int result_count = 0;
165 for (int i = 0; i < row_count; ++i) { 194 for (int i = 0; i < folder_count; ++i) {
166 const QStandardItem* child_file = item_model->item(i, 0); 195 folder = item_model->item(i, 0);
167 const QString file_path = 196 const QModelIndex folder_index = folder->index();
168 child_file->data(GameListItemPath::FullPathRole).toString().toLower(); 197 const int children_count = folder->rowCount();
169 const QString file_title = 198 for (int j = 0; j < children_count; ++j) {
170 child_file->data(GameListItemPath::TitleRole).toString().toLower(); 199 ++children_total;
171 const QString file_program_id = 200 const QStandardItem* child = folder->child(j, 0);
172 child_file->data(GameListItemPath::ProgramIdRole).toString().toLower(); 201 const QString file_path =
173 202 child->data(GameListItemPath::FullPathRole).toString().toLower();
174 // Only items which filename in combination with its title contains all words 203 const QString file_title =
175 // that are in the searchfield will be visible in the gamelist 204 child->data(GameListItemPath::TitleRole).toString().toLower();
176 // The search is case insensitive because of toLower() 205 const QString file_program_id =
177 // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent 206 child->data(GameListItemPath::ProgramIdRole).toString().toLower();
178 // multiple conversions of edit_filter_text for each game in the gamelist 207
179 const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + 208 // Only items which filename in combination with its title contains all words
180 QLatin1Char{' '} + file_title; 209 // that are in the searchfield will be visible in the gamelist
181 if (ContainsAllWords(file_name, edit_filter_text) || 210 // The search is case insensitive because of toLower()
182 (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { 211 // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
183 tree_view->setRowHidden(i, root_index, false); 212 // multiple conversions of edit_filter_text for each game in the gamelist
184 ++result_count; 213 const QString file_name =
185 } else { 214 file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
186 tree_view->setRowHidden(i, root_index, true); 215 file_title;
216 if (ContainsAllWords(file_name, edit_filter_text) ||
217 (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
218 tree_view->setRowHidden(j, folder_index, false);
219 ++result_count;
220 } else {
221 tree_view->setRowHidden(j, folder_index, true);
222 }
223 search_field->setFilterResult(result_count, children_total);
187 } 224 }
188 search_field->setFilterResult(result_count, row_count); 225 }
226 }
227}
228
229void GameList::onUpdateThemedIcons() {
230 for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
231 QStandardItem* child = item_model->invisibleRootItem()->child(i);
232
233 const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
234 switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
235 case GameListItemType::SdmcDir:
236 child->setData(
237 QIcon::fromTheme(QStringLiteral("sd_card"))
238 .pixmap(icon_size)
239 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
240 Qt::DecorationRole);
241 break;
242 case GameListItemType::UserNandDir:
243 child->setData(
244 QIcon::fromTheme(QStringLiteral("chip"))
245 .pixmap(icon_size)
246 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
247 Qt::DecorationRole);
248 break;
249 case GameListItemType::SysNandDir:
250 child->setData(
251 QIcon::fromTheme(QStringLiteral("chip"))
252 .pixmap(icon_size)
253 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
254 Qt::DecorationRole);
255 break;
256 case GameListItemType::CustomDir: {
257 const UISettings::GameDir* game_dir =
258 child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
259 const QString icon_name = QFileInfo::exists(game_dir->path)
260 ? QStringLiteral("folder")
261 : QStringLiteral("bad_folder");
262 child->setData(
263 QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
264 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
265 Qt::DecorationRole);
266 break;
267 }
268 case GameListItemType::AddDir:
269 child->setData(
270 QIcon::fromTheme(QStringLiteral("plus"))
271 .pixmap(icon_size)
272 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
273 Qt::DecorationRole);
274 break;
189 } 275 }
190 } 276 }
191} 277}
@@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
214 tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); 300 tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
215 tree_view->setSortingEnabled(true); 301 tree_view->setSortingEnabled(true);
216 tree_view->setEditTriggers(QHeaderView::NoEditTriggers); 302 tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
217 tree_view->setUniformRowHeights(true);
218 tree_view->setContextMenuPolicy(Qt::CustomContextMenu); 303 tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
219 tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); 304 tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
220 305
@@ -230,12 +315,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
230 item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); 315 item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type"));
231 item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); 316 item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size"));
232 } 317 }
318 item_model->setSortRole(GameListItemPath::TitleRole);
233 319
320 connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);
234 connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); 321 connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
235 connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); 322 connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
323 connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
324 connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
236 325
237 // We must register all custom types with the Qt Automoc system so that we are able to use it 326 // We must register all custom types with the Qt Automoc system so that we are able to use
238 // with signals/slots. In this case, QList falls under the umbrells of custom types. 327 // it with signals/slots. In this case, QList falls under the umbrells of custom types.
239 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); 328 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
240 329
241 layout->setContentsMargins(0, 0, 0, 0); 330 layout->setContentsMargins(0, 0, 0, 0);
@@ -263,38 +352,68 @@ void GameList::clearFilter() {
263 search_field->clear(); 352 search_field->clear();
264} 353}
265 354
266void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { 355void GameList::AddDirEntry(GameListDir* entry_items) {
267 item_model->invisibleRootItem()->appendRow(entry_items); 356 item_model->invisibleRootItem()->appendRow(entry_items);
357 tree_view->setExpanded(
358 entry_items->index(),
359 entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
268} 360}
269 361
270void GameList::ValidateEntry(const QModelIndex& item) { 362void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
271 // We don't care about the individual QStandardItem that was selected, but its row. 363 parent->appendRow(entry_items);
272 const int row = item_model->itemFromIndex(item)->row(); 364}
273 const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
274 const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString();
275
276 if (file_path.isEmpty())
277 return;
278
279 if (!QFileInfo::exists(file_path))
280 return;
281 365
282 const QFileInfo file_info{file_path}; 366void GameList::ValidateEntry(const QModelIndex& item) {
283 if (file_info.isDir()) { 367 const auto selected = item.sibling(item.row(), 0);
284 const QDir dir{file_path}; 368
285 const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); 369 switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
286 if (matching_main.size() == 1) { 370 case GameListItemType::Game: {
287 emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); 371 const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
372 if (file_path.isEmpty())
373 return;
374 const QFileInfo file_info(file_path);
375 if (!file_info.exists())
376 return;
377
378 if (file_info.isDir()) {
379 const QDir dir{file_path};
380 const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
381 if (matching_main.size() == 1) {
382 emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
383 }
384 return;
288 } 385 }
289 return; 386
387 // Users usually want to run a different game after closing one
388 search_field->clear();
389 emit GameChosen(file_path);
390 break;
290 } 391 }
392 case GameListItemType::AddDir:
393 emit AddDirectory();
394 break;
395 }
396}
291 397
292 // Users usually want to run a diffrent game after closing one 398bool GameList::isEmpty() const {
293 search_field->clear(); 399 for (int i = 0; i < item_model->rowCount(); i++) {
294 emit GameChosen(file_path); 400 const QStandardItem* child = item_model->invisibleRootItem()->child(i);
401 const auto type = static_cast<GameListItemType>(child->type());
402 if (!child->hasChildren() &&
403 (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
404 type == GameListItemType::SysNandDir)) {
405 item_model->invisibleRootItem()->removeRow(child->row());
406 i--;
407 };
408 }
409 return !item_model->invisibleRootItem()->hasChildren();
295} 410}
296 411
297void GameList::DonePopulating(QStringList watch_list) { 412void GameList::DonePopulating(QStringList watch_list) {
413 emit ShowList(!isEmpty());
414
415 item_model->invisibleRootItem()->appendRow(new GameListAddDir());
416
298 // Clear out the old directories to watch for changes and add the new ones 417 // Clear out the old directories to watch for changes and add the new ones
299 auto watch_dirs = watcher->directories(); 418 auto watch_dirs = watcher->directories();
300 if (!watch_dirs.isEmpty()) { 419 if (!watch_dirs.isEmpty()) {
@@ -311,9 +430,13 @@ void GameList::DonePopulating(QStringList watch_list) {
311 QCoreApplication::processEvents(); 430 QCoreApplication::processEvents();
312 } 431 }
313 tree_view->setEnabled(true); 432 tree_view->setEnabled(true);
314 int rowCount = tree_view->model()->rowCount(); 433 const int folder_count = tree_view->model()->rowCount();
315 search_field->setFilterResult(rowCount, rowCount); 434 int children_total = 0;
316 if (rowCount > 0) { 435 for (int i = 0; i < folder_count; ++i) {
436 children_total += item_model->item(i, 0)->rowCount();
437 }
438 search_field->setFilterResult(children_total, children_total);
439 if (children_total > 0) {
317 search_field->setFocus(); 440 search_field->setFocus();
318 } 441 }
319} 442}
@@ -323,12 +446,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
323 if (!item.isValid()) 446 if (!item.isValid())
324 return; 447 return;
325 448
326 int row = item_model->itemFromIndex(item)->row(); 449 const auto selected = item.sibling(item.row(), 0);
327 QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
328 u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong();
329 std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString();
330
331 QMenu context_menu; 450 QMenu context_menu;
451 switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
452 case GameListItemType::Game:
453 AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
454 selected.data(GameListItemPath::FullPathRole).toString().toStdString());
455 break;
456 case GameListItemType::CustomDir:
457 AddPermDirPopup(context_menu, selected);
458 AddCustomDirPopup(context_menu, selected);
459 break;
460 case GameListItemType::SdmcDir:
461 case GameListItemType::UserNandDir:
462 case GameListItemType::SysNandDir:
463 AddPermDirPopup(context_menu, selected);
464 break;
465 }
466 context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
467}
468
469void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) {
332 QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); 470 QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
333 QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); 471 QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));
334 QAction* open_transferable_shader_cache = 472 QAction* open_transferable_shader_cache =
@@ -344,19 +482,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
344 auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); 482 auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
345 navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); 483 navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
346 484
347 connect(open_save_location, &QAction::triggered, 485 connect(open_save_location, &QAction::triggered, [this, program_id]() {
348 [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); 486 emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData);
349 connect(open_lfs_location, &QAction::triggered, 487 });
350 [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); 488 connect(open_lfs_location, &QAction::triggered, [this, program_id]() {
489 emit OpenFolderRequested(program_id, GameListOpenTarget::ModData);
490 });
351 connect(open_transferable_shader_cache, &QAction::triggered, 491 connect(open_transferable_shader_cache, &QAction::triggered,
352 [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); 492 [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
353 connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); 493 connect(dump_romfs, &QAction::triggered,
354 connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); 494 [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); });
355 connect(navigate_to_gamedb_entry, &QAction::triggered, 495 connect(copy_tid, &QAction::triggered,
356 [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); 496 [this, program_id]() { emit CopyTIDRequested(program_id); });
357 connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); 497 connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
498 emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
499 });
500 connect(properties, &QAction::triggered,
501 [this, path]() { emit OpenPerGameGeneralRequested(path); });
502};
503
504void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
505 UISettings::GameDir& game_dir =
506 *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
507
508 QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
509 QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
510
511 deep_scan->setCheckable(true);
512 deep_scan->setChecked(game_dir.deep_scan);
513
514 connect(deep_scan, &QAction::triggered, [this, &game_dir] {
515 game_dir.deep_scan = !game_dir.deep_scan;
516 PopulateAsync(UISettings::values.game_dirs);
517 });
518 connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] {
519 UISettings::values.game_dirs.removeOne(game_dir);
520 item_model->invisibleRootItem()->removeRow(selected.row());
521 });
522}
358 523
359 context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); 524void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
525 UISettings::GameDir& game_dir =
526 *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
527
528 QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up"));
529 QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down "));
530 QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location"));
531
532 const int row = selected.row();
533
534 move_up->setEnabled(row > 0);
535 move_down->setEnabled(row < item_model->rowCount() - 2);
536
537 connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] {
538 // find the indices of the items in settings and swap them
539 std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
540 UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
541 *selected.sibling(row - 1, 0)
542 .data(GameListDir::GameDirRole)
543 .value<UISettings::GameDir*>())]);
544 // move the treeview items
545 QList<QStandardItem*> item = item_model->takeRow(row);
546 item_model->invisibleRootItem()->insertRow(row - 1, item);
547 tree_view->setExpanded(selected, game_dir.expanded);
548 });
549
550 connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] {
551 // find the indices of the items in settings and swap them
552 std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
553 UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
554 *selected.sibling(row + 1, 0)
555 .data(GameListDir::GameDirRole)
556 .value<UISettings::GameDir*>())]);
557 // move the treeview items
558 const QList<QStandardItem*> item = item_model->takeRow(row);
559 item_model->invisibleRootItem()->insertRow(row + 1, item);
560 tree_view->setExpanded(selected, game_dir.expanded);
561 });
562
563 connect(open_directory_location, &QAction::triggered,
564 [this, game_dir] { emit OpenDirectory(game_dir.path); });
360} 565}
361 566
362void GameList::LoadCompatibilityList() { 567void GameList::LoadCompatibilityList() {
@@ -403,14 +608,7 @@ void GameList::LoadCompatibilityList() {
403 } 608 }
404} 609}
405 610
406void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { 611void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
407 const QFileInfo dir_info{dir_path};
408 if (!dir_info.exists() || !dir_info.isDir()) {
409 LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString());
410 search_field->setFilterResult(0, 0);
411 return;
412 }
413
414 tree_view->setEnabled(false); 612 tree_view->setEnabled(false);
415 613
416 // Update the columns in case UISettings has changed 614 // Update the columns in case UISettings has changed
@@ -433,17 +631,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
433 631
434 // Delete any rows that might already exist if we're repopulating 632 // Delete any rows that might already exist if we're repopulating
435 item_model->removeRows(0, item_model->rowCount()); 633 item_model->removeRows(0, item_model->rowCount());
634 search_field->clear();
436 635
437 emit ShouldCancelWorker(); 636 emit ShouldCancelWorker();
438 637
439 GameListWorker* worker = 638 GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);
440 new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
441 639
442 connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); 640 connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
641 connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
642 Qt::QueuedConnection);
443 connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, 643 connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
444 Qt::QueuedConnection); 644 Qt::QueuedConnection);
445 // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel 645 // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
446 // without delay. 646 // cancel without delay.
447 connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, 647 connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
448 Qt::DirectConnection); 648 Qt::DirectConnection);
449 649
@@ -471,10 +671,40 @@ const QStringList GameList::supported_file_extensions = {
471 QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; 671 QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
472 672
473void GameList::RefreshGameDirectory() { 673void GameList::RefreshGameDirectory() {
474 if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { 674 if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
475 LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); 675 LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
476 search_field->clear(); 676 PopulateAsync(UISettings::values.game_dirs);
477 PopulateAsync(UISettings::values.game_directory_path,
478 UISettings::values.game_directory_deepscan);
479 } 677 }
480} 678}
679
680GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
681 connect(parent, &GMainWindow::UpdateThemedIcons, this,
682 &GameListPlaceholder::onUpdateThemedIcons);
683
684 layout = new QVBoxLayout;
685 image = new QLabel;
686 text = new QLabel;
687 layout->setAlignment(Qt::AlignCenter);
688 image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
689
690 text->setText(tr("Double-click to add a new folder to the game list"));
691 QFont font = text->font();
692 font.setPointSize(20);
693 text->setFont(font);
694 text->setAlignment(Qt::AlignHCenter);
695 image->setAlignment(Qt::AlignHCenter);
696
697 layout->addWidget(image);
698 layout->addWidget(text);
699 setLayout(layout);
700}
701
702GameListPlaceholder::~GameListPlaceholder() = default;
703
704void GameListPlaceholder::onUpdateThemedIcons() {
705 image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
706}
707
708void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
709 emit GameListPlaceholder::AddDirectory();
710}
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 @@
8#include <QHBoxLayout> 8#include <QHBoxLayout>
9#include <QLabel> 9#include <QLabel>
10#include <QLineEdit> 10#include <QLineEdit>
11#include <QList>
11#include <QModelIndex> 12#include <QModelIndex>
12#include <QSettings> 13#include <QSettings>
13#include <QStandardItem> 14#include <QStandardItem>
@@ -16,13 +17,16 @@
16#include <QToolButton> 17#include <QToolButton>
17#include <QTreeView> 18#include <QTreeView>
18#include <QVBoxLayout> 19#include <QVBoxLayout>
20#include <QVector>
19#include <QWidget> 21#include <QWidget>
20 22
21#include "common/common_types.h" 23#include "common/common_types.h"
24#include "uisettings.h"
22#include "yuzu/compatibility_list.h" 25#include "yuzu/compatibility_list.h"
23 26
24class GameListWorker; 27class GameListWorker;
25class GameListSearchField; 28class GameListSearchField;
29class GameListDir;
26class GMainWindow; 30class GMainWindow;
27 31
28namespace FileSys { 32namespace FileSys {
@@ -52,12 +56,14 @@ public:
52 FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr); 56 FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
53 ~GameList() override; 57 ~GameList() override;
54 58
59 QString getLastFilterResultItem() const;
55 void clearFilter(); 60 void clearFilter();
56 void setFilterFocus(); 61 void setFilterFocus();
57 void setFilterVisible(bool visibility); 62 void setFilterVisible(bool visibility);
63 bool isEmpty() const;
58 64
59 void LoadCompatibilityList(); 65 void LoadCompatibilityList();
60 void PopulateAsync(const QString& dir_path, bool deep_scan); 66 void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
61 67
62 void SaveInterfaceLayout(); 68 void SaveInterfaceLayout();
63 void LoadInterfaceLayout(); 69 void LoadInterfaceLayout();
@@ -74,19 +80,29 @@ signals:
74 void NavigateToGamedbEntryRequested(u64 program_id, 80 void NavigateToGamedbEntryRequested(u64 program_id,
75 const CompatibilityList& compatibility_list); 81 const CompatibilityList& compatibility_list);
76 void OpenPerGameGeneralRequested(const std::string& file); 82 void OpenPerGameGeneralRequested(const std::string& file);
83 void OpenDirectory(const QString& directory);
84 void AddDirectory();
85 void ShowList(bool show);
77 86
78private slots: 87private slots:
88 void onItemExpanded(const QModelIndex& item);
79 void onTextChanged(const QString& new_text); 89 void onTextChanged(const QString& new_text);
80 void onFilterCloseClicked(); 90 void onFilterCloseClicked();
91 void onUpdateThemedIcons();
81 92
82private: 93private:
83 void AddEntry(const QList<QStandardItem*>& entry_items); 94 void AddDirEntry(GameListDir* entry_items);
95 void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
84 void ValidateEntry(const QModelIndex& item); 96 void ValidateEntry(const QModelIndex& item);
85 void DonePopulating(QStringList watch_list); 97 void DonePopulating(QStringList watch_list);
86 98
87 void PopupContextMenu(const QPoint& menu_location);
88 void RefreshGameDirectory(); 99 void RefreshGameDirectory();
89 100
101 void PopupContextMenu(const QPoint& menu_location);
102 void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path);
103 void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
104 void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
105
90 std::shared_ptr<FileSys::VfsFilesystem> vfs; 106 std::shared_ptr<FileSys::VfsFilesystem> vfs;
91 FileSys::ManualContentProvider* provider; 107 FileSys::ManualContentProvider* provider;
92 GameListSearchField* search_field; 108 GameListSearchField* search_field;
@@ -102,3 +118,24 @@ private:
102}; 118};
103 119
104Q_DECLARE_METATYPE(GameListOpenTarget); 120Q_DECLARE_METATYPE(GameListOpenTarget);
121
122class GameListPlaceholder : public QWidget {
123 Q_OBJECT
124public:
125 explicit GameListPlaceholder(GMainWindow* parent = nullptr);
126 ~GameListPlaceholder();
127
128signals:
129 void AddDirectory();
130
131private slots:
132 void onUpdateThemedIcons();
133
134protected:
135 void mouseDoubleClickEvent(QMouseEvent* event) override;
136
137private:
138 QVBoxLayout* layout = nullptr;
139 QLabel* image = nullptr;
140 QLabel* text = nullptr;
141};
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 @@
10#include <utility> 10#include <utility>
11 11
12#include <QCoreApplication> 12#include <QCoreApplication>
13#include <QFileInfo>
13#include <QImage> 14#include <QImage>
14#include <QObject> 15#include <QObject>
15#include <QStandardItem> 16#include <QStandardItem>
@@ -22,6 +23,17 @@
22#include "yuzu/uisettings.h" 23#include "yuzu/uisettings.h"
23#include "yuzu/util/util.h" 24#include "yuzu/util/util.h"
24 25
26enum class GameListItemType {
27 Game = QStandardItem::UserType + 1,
28 CustomDir = QStandardItem::UserType + 2,
29 SdmcDir = QStandardItem::UserType + 3,
30 UserNandDir = QStandardItem::UserType + 4,
31 SysNandDir = QStandardItem::UserType + 5,
32 AddDir = QStandardItem::UserType + 6
33};
34
35Q_DECLARE_METATYPE(GameListItemType);
36
25/** 37/**
26 * Gets the default icon (for games without valid title metadata) 38 * Gets the default icon (for games without valid title metadata)
27 * @param size The desired width and height of the default icon. 39 * @param size The desired width and height of the default icon.
@@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) {
36class GameListItem : public QStandardItem { 48class GameListItem : public QStandardItem {
37 49
38public: 50public:
51 // used to access type from item index
52 static const int TypeRole = Qt::UserRole + 1;
53 static const int SortRole = Qt::UserRole + 2;
39 GameListItem() = default; 54 GameListItem() = default;
40 explicit GameListItem(const QString& string) : QStandardItem(string) {} 55 GameListItem(const QString& string) : QStandardItem(string) {
56 setData(string, SortRole);
57 }
41}; 58};
42 59
43/** 60/**
@@ -48,14 +65,15 @@ public:
48 */ 65 */
49class GameListItemPath : public GameListItem { 66class GameListItemPath : public GameListItem {
50public: 67public:
51 static const int FullPathRole = Qt::UserRole + 1; 68 static const int TitleRole = SortRole;
52 static const int TitleRole = Qt::UserRole + 2; 69 static const int FullPathRole = SortRole + 1;
53 static const int ProgramIdRole = Qt::UserRole + 3; 70 static const int ProgramIdRole = SortRole + 2;
54 static const int FileTypeRole = Qt::UserRole + 4; 71 static const int FileTypeRole = SortRole + 3;
55 72
56 GameListItemPath() = default; 73 GameListItemPath() = default;
57 GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, 74 GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
58 const QString& game_name, const QString& game_type, u64 program_id) { 75 const QString& game_name, const QString& game_type, u64 program_id) {
76 setData(type(), TypeRole);
59 setData(game_path, FullPathRole); 77 setData(game_path, FullPathRole);
60 setData(game_name, TitleRole); 78 setData(game_name, TitleRole);
61 setData(qulonglong(program_id), ProgramIdRole); 79 setData(qulonglong(program_id), ProgramIdRole);
@@ -72,6 +90,10 @@ public:
72 setData(picture, Qt::DecorationRole); 90 setData(picture, Qt::DecorationRole);
73 } 91 }
74 92
93 int type() const override {
94 return static_cast<int>(GameListItemType::Game);
95 }
96
75 QVariant data(int role) const override { 97 QVariant data(int role) const override {
76 if (role == Qt::DisplayRole) { 98 if (role == Qt::DisplayRole) {
77 std::string filename; 99 std::string filename;
@@ -103,9 +125,11 @@ public:
103class GameListItemCompat : public GameListItem { 125class GameListItemCompat : public GameListItem {
104 Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) 126 Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
105public: 127public:
106 static const int CompatNumberRole = Qt::UserRole + 1; 128 static const int CompatNumberRole = SortRole;
107 GameListItemCompat() = default; 129 GameListItemCompat() = default;
108 explicit GameListItemCompat(const QString& compatibility) { 130 explicit GameListItemCompat(const QString& compatibility) {
131 setData(type(), TypeRole);
132
109 struct CompatStatus { 133 struct CompatStatus {
110 QString color; 134 QString color;
111 const char* text; 135 const char* text;
@@ -135,6 +159,10 @@ public:
135 setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); 159 setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
136 } 160 }
137 161
162 int type() const override {
163 return static_cast<int>(GameListItemType::Game);
164 }
165
138 bool operator<(const QStandardItem& other) const override { 166 bool operator<(const QStandardItem& other) const override {
139 return data(CompatNumberRole) < other.data(CompatNumberRole); 167 return data(CompatNumberRole) < other.data(CompatNumberRole);
140 } 168 }
@@ -146,12 +174,12 @@ public:
146 * human-readable string representation will be displayed to the user. 174 * human-readable string representation will be displayed to the user.
147 */ 175 */
148class GameListItemSize : public GameListItem { 176class GameListItemSize : public GameListItem {
149
150public: 177public:
151 static const int SizeRole = Qt::UserRole + 1; 178 static const int SizeRole = SortRole;
152 179
153 GameListItemSize() = default; 180 GameListItemSize() = default;
154 explicit GameListItemSize(const qulonglong size_bytes) { 181 explicit GameListItemSize(const qulonglong size_bytes) {
182 setData(type(), TypeRole);
155 setData(size_bytes, SizeRole); 183 setData(size_bytes, SizeRole);
156 } 184 }
157 185
@@ -167,6 +195,10 @@ public:
167 } 195 }
168 } 196 }
169 197
198 int type() const override {
199 return static_cast<int>(GameListItemType::Game);
200 }
201
170 /** 202 /**
171 * This operator is, in practice, only used by the TreeView sorting systems. 203 * This operator is, in practice, only used by the TreeView sorting systems.
172 * Override it so that it will correctly sort by numerical value instead of by string 204 * Override it so that it will correctly sort by numerical value instead of by string
@@ -177,6 +209,82 @@ public:
177 } 209 }
178}; 210};
179 211
212class GameListDir : public GameListItem {
213public:
214 static const int GameDirRole = Qt::UserRole + 2;
215
216 explicit GameListDir(UISettings::GameDir& directory,
217 GameListItemType dir_type = GameListItemType::CustomDir)
218 : dir_type{dir_type} {
219 setData(type(), TypeRole);
220
221 UISettings::GameDir* game_dir = &directory;
222 setData(QVariant::fromValue(game_dir), GameDirRole);
223
224 const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
225 switch (dir_type) {
226 case GameListItemType::SdmcDir:
227 setData(
228 QIcon::fromTheme(QStringLiteral("sd_card"))
229 .pixmap(icon_size)
230 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
231 Qt::DecorationRole);
232 setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
233 break;
234 case GameListItemType::UserNandDir:
235 setData(
236 QIcon::fromTheme(QStringLiteral("chip"))
237 .pixmap(icon_size)
238 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
239 Qt::DecorationRole);
240 setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
241 break;
242 case GameListItemType::SysNandDir:
243 setData(
244 QIcon::fromTheme(QStringLiteral("chip"))
245 .pixmap(icon_size)
246 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
247 Qt::DecorationRole);
248 setData(QObject::tr("System Titles"), Qt::DisplayRole);
249 break;
250 case GameListItemType::CustomDir:
251 const QString icon_name = QFileInfo::exists(game_dir->path)
252 ? QStringLiteral("folder")
253 : QStringLiteral("bad_folder");
254 setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
255 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
256 Qt::DecorationRole);
257 setData(game_dir->path, Qt::DisplayRole);
258 break;
259 };
260 };
261
262 int type() const override {
263 return static_cast<int>(dir_type);
264 }
265
266private:
267 GameListItemType dir_type;
268};
269
270class GameListAddDir : public GameListItem {
271public:
272 explicit GameListAddDir() {
273 setData(type(), TypeRole);
274
275 const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
276 setData(QIcon::fromTheme(QStringLiteral("plus"))
277 .pixmap(icon_size)
278 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
279 Qt::DecorationRole);
280 setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
281 }
282
283 int type() const override {
284 return static_cast<int>(GameListItemType::AddDir);
285 }
286};
287
180class GameList; 288class GameList;
181class QHBoxLayout; 289class QHBoxLayout;
182class QTreeView; 290class QTreeView;
@@ -208,6 +316,9 @@ private:
208 // EventFilter in order to process systemkeys while editing the searchfield 316 // EventFilter in order to process systemkeys while editing the searchfield
209 bool eventFilter(QObject* obj, QEvent* event) override; 317 bool eventFilter(QObject* obj, QEvent* event) override;
210 }; 318 };
319 int visible;
320 int total;
321
211 QHBoxLayout* layout_filter = nullptr; 322 QHBoxLayout* layout_filter = nullptr;
212 QTreeView* tree_view = nullptr; 323 QTreeView* tree_view = nullptr;
213 QLabel* label_filter = nullptr; 324 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<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
223} // Anonymous namespace 223} // Anonymous namespace
224 224
225GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, 225GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
226 FileSys::ManualContentProvider* provider, QString dir_path, 226 FileSys::ManualContentProvider* provider,
227 bool deep_scan, const CompatibilityList& compatibility_list) 227 QVector<UISettings::GameDir>& game_dirs,
228 : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan), 228 const CompatibilityList& compatibility_list)
229 : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),
229 compatibility_list(compatibility_list) {} 230 compatibility_list(compatibility_list) {}
230 231
231GameListWorker::~GameListWorker() = default; 232GameListWorker::~GameListWorker() = default;
232 233
233void GameListWorker::AddTitlesToGameList() { 234void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
234 const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>( 235 using namespace FileSys;
235 Core::System::GetInstance().GetContentProvider()); 236
236 const auto installed_games = cache.ListEntriesFilterOrigin( 237 const auto& cache =
237 std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); 238 dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider());
239
240 std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games;
241 installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
242 ContentRecordType::Program);
243
244 if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
245 installed_games = cache.ListEntriesFilterOrigin(
246 ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
247 } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
248 installed_games = cache.ListEntriesFilterOrigin(
249 ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
250 } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
251 installed_games = cache.ListEntriesFilterOrigin(
252 ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
253 }
238 254
239 for (const auto& [slot, game] : installed_games) { 255 for (const auto& [slot, game] : installed_games) {
240 if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) 256 if (slot == ContentProviderUnionSlot::FrontendManual)
241 continue; 257 continue;
242 258
243 const auto file = cache.GetEntryUnparsed(game.title_id, game.type); 259 const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
@@ -250,21 +266,22 @@ void GameListWorker::AddTitlesToGameList() {
250 u64 program_id = 0; 266 u64 program_id = 0;
251 loader->ReadProgramId(program_id); 267 loader->ReadProgramId(program_id);
252 268
253 const FileSys::PatchManager patch{program_id}; 269 const PatchManager patch{program_id};
254 const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); 270 const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
255 if (control != nullptr) 271 if (control != nullptr)
256 GetMetadataFromControlNCA(patch, *control, icon, name); 272 GetMetadataFromControlNCA(patch, *control, icon, name);
257 273
258 emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, 274 emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
259 compatibility_list, patch)); 275 compatibility_list, patch),
276 parent_dir);
260 } 277 }
261} 278}
262 279
263void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, 280void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
264 unsigned int recursion) { 281 unsigned int recursion, GameListDir* parent_dir) {
265 const auto callback = [this, target, recursion](u64* num_entries_out, 282 const auto callback = [this, target, recursion,
266 const std::string& directory, 283 parent_dir](u64* num_entries_out, const std::string& directory,
267 const std::string& virtual_name) -> bool { 284 const std::string& virtual_name) -> bool {
268 if (stop_processing) { 285 if (stop_processing) {
269 // Breaks the callback loop. 286 // Breaks the callback loop.
270 return false; 287 return false;
@@ -317,11 +334,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
317 const FileSys::PatchManager patch{program_id}; 334 const FileSys::PatchManager patch{program_id};
318 335
319 emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, 336 emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
320 compatibility_list, patch)); 337 compatibility_list, patch),
338 parent_dir);
321 } 339 }
322 } else if (is_dir && recursion > 0) { 340 } else if (is_dir && recursion > 0) {
323 watch_list.append(QString::fromStdString(physical_name)); 341 watch_list.append(QString::fromStdString(physical_name));
324 ScanFileSystem(target, physical_name, recursion - 1); 342 ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
325 } 343 }
326 344
327 return true; 345 return true;
@@ -332,12 +350,32 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
332 350
333void GameListWorker::run() { 351void GameListWorker::run() {
334 stop_processing = false; 352 stop_processing = false;
335 watch_list.append(dir_path); 353
336 provider->ClearAllEntries(); 354 for (UISettings::GameDir& game_dir : game_dirs) {
337 ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), 355 if (game_dir.path == QStringLiteral("SDMC")) {
338 deep_scan ? 256 : 0); 356 auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
339 AddTitlesToGameList(); 357 emit DirEntryReady({game_list_dir});
340 ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); 358 AddTitlesToGameList(game_list_dir);
359 } else if (game_dir.path == QStringLiteral("UserNAND")) {
360 auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
361 emit DirEntryReady({game_list_dir});
362 AddTitlesToGameList(game_list_dir);
363 } else if (game_dir.path == QStringLiteral("SysNAND")) {
364 auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
365 emit DirEntryReady({game_list_dir});
366 AddTitlesToGameList(game_list_dir);
367 } else {
368 watch_list.append(game_dir.path);
369 auto* const game_list_dir = new GameListDir(game_dir);
370 emit DirEntryReady({game_list_dir});
371 provider->ClearAllEntries();
372 ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2,
373 game_list_dir);
374 ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
375 game_dir.deep_scan ? 256 : 0, game_list_dir);
376 }
377 };
378
341 emit Finished(watch_list); 379 emit Finished(watch_list);
342} 380}
343 381
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 @@
14#include <QObject> 14#include <QObject>
15#include <QRunnable> 15#include <QRunnable>
16#include <QString> 16#include <QString>
17#include <QVector>
17 18
18#include "common/common_types.h" 19#include "common/common_types.h"
19#include "yuzu/compatibility_list.h" 20#include "yuzu/compatibility_list.h"
@@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable {
33 Q_OBJECT 34 Q_OBJECT
34 35
35public: 36public:
36 GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, 37 explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
37 FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, 38 FileSys::ManualContentProvider* provider,
38 const CompatibilityList& compatibility_list); 39 QVector<UISettings::GameDir>& game_dirs,
40 const CompatibilityList& compatibility_list);
39 ~GameListWorker() override; 41 ~GameListWorker() override;
40 42
41 /// Starts the processing of directory tree information. 43 /// Starts the processing of directory tree information.
@@ -48,31 +50,33 @@ signals:
48 /** 50 /**
49 * The `EntryReady` signal is emitted once an entry has been prepared and is ready 51 * The `EntryReady` signal is emitted once an entry has been prepared and is ready
50 * to be added to the game list. 52 * to be added to the game list.
51 * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. 53 * @param entry_items a list with `QStandardItem`s that make up the columns of the new
54 * entry.
52 */ 55 */
53 void EntryReady(QList<QStandardItem*> entry_items); 56 void DirEntryReady(GameListDir* entry_items);
57 void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
54 58
55 /** 59 /**
56 * After the worker has traversed the game directory looking for entries, this signal is emitted 60 * After the worker has traversed the game directory looking for entries, this signal is
57 * with a list of folders that should be watched for changes as well. 61 * emitted with a list of folders that should be watched for changes as well.
58 */ 62 */
59 void Finished(QStringList watch_list); 63 void Finished(QStringList watch_list);
60 64
61private: 65private:
62 void AddTitlesToGameList(); 66 void AddTitlesToGameList(GameListDir* parent_dir);
63 67
64 enum class ScanTarget { 68 enum class ScanTarget {
65 FillManualContentProvider, 69 FillManualContentProvider,
66 PopulateGameList, 70 PopulateGameList,
67 }; 71 };
68 72
69 void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); 73 void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion,
74 GameListDir* parent_dir);
70 75
71 std::shared_ptr<FileSys::VfsFilesystem> vfs; 76 std::shared_ptr<FileSys::VfsFilesystem> vfs;
72 FileSys::ManualContentProvider* provider; 77 FileSys::ManualContentProvider* provider;
73 QStringList watch_list; 78 QStringList watch_list;
74 QString dir_path;
75 bool deep_scan;
76 const CompatibilityList& compatibility_list; 79 const CompatibilityList& compatibility_list;
80 QVector<UISettings::GameDir>& game_dirs;
77 std::atomic_bool stop_processing; 81 std::atomic_bool stop_processing;
78}; 82};
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()
216 OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); 216 OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
217 217
218 game_list->LoadCompatibilityList(); 218 game_list->LoadCompatibilityList();
219 game_list->PopulateAsync(UISettings::values.game_directory_path, 219 game_list->PopulateAsync(UISettings::values.game_dirs);
220 UISettings::values.game_directory_deepscan);
221 220
222 // Show one-time "callout" messages to the user 221 // Show one-time "callout" messages to the user
223 ShowTelemetryCallout(); 222 ShowTelemetryCallout();
@@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() {
427 game_list = new GameList(vfs, provider.get(), this); 426 game_list = new GameList(vfs, provider.get(), this);
428 ui.horizontalLayout->addWidget(game_list); 427 ui.horizontalLayout->addWidget(game_list);
429 428
429 game_list_placeholder = new GameListPlaceholder(this);
430 ui.horizontalLayout->addWidget(game_list_placeholder);
431 game_list_placeholder->setVisible(false);
432
430 loading_screen = new LoadingScreen(this); 433 loading_screen = new LoadingScreen(this);
431 loading_screen->hide(); 434 loading_screen->hide();
432 ui.horizontalLayout->addWidget(loading_screen); 435 ui.horizontalLayout->addWidget(loading_screen);
@@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() {
660 663
661void GMainWindow::ConnectWidgetEvents() { 664void GMainWindow::ConnectWidgetEvents() {
662 connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); 665 connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
666 connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
663 connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); 667 connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
664 connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, 668 connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,
665 &GMainWindow::OnTransferableShaderCacheOpenFile); 669 &GMainWindow::OnTransferableShaderCacheOpenFile);
@@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() {
667 connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); 671 connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
668 connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, 672 connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
669 &GMainWindow::OnGameListNavigateToGamedbEntry); 673 &GMainWindow::OnGameListNavigateToGamedbEntry);
674 connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
675 connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
676 &GMainWindow::OnGameListAddDirectory);
677 connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
678
670 connect(game_list, &GameList::OpenPerGameGeneralRequested, this, 679 connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
671 &GMainWindow::OnGameListOpenPerGameProperties); 680 &GMainWindow::OnGameListOpenPerGameProperties);
672 681
@@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() {
684 connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); 693 connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
685 connect(ui.action_Install_File_NAND, &QAction::triggered, this, 694 connect(ui.action_Install_File_NAND, &QAction::triggered, this,
686 &GMainWindow::OnMenuInstallToNAND); 695 &GMainWindow::OnMenuInstallToNAND);
687 connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
688 &GMainWindow::OnMenuSelectGameListRoot);
689 connect(ui.action_Select_NAND_Directory, &QAction::triggered, this, 696 connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
690 [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); }); 697 [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
691 connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, 698 connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
@@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) {
950 // Update the GUI 957 // Update the GUI
951 if (ui.action_Single_Window_Mode->isChecked()) { 958 if (ui.action_Single_Window_Mode->isChecked()) {
952 game_list->hide(); 959 game_list->hide();
960 game_list_placeholder->hide();
953 } 961 }
954 status_bar_update_timer.start(2000); 962 status_bar_update_timer.start(2000);
955 963
@@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() {
1007 render_window->hide(); 1015 render_window->hide();
1008 loading_screen->hide(); 1016 loading_screen->hide();
1009 loading_screen->Clear(); 1017 loading_screen->Clear();
1010 game_list->show(); 1018 if (game_list->isEmpty())
1019 game_list_placeholder->show();
1020 else
1021 game_list->show();
1011 game_list->setFilterFocus(); 1022 game_list->setFilterFocus();
1012 1023
1013 UpdateWindowTitle(); 1024 UpdateWindowTitle();
@@ -1298,6 +1309,47 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
1298 QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); 1309 QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
1299} 1310}
1300 1311
1312void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
1313 QString path;
1314 if (directory == QStringLiteral("SDMC")) {
1315 path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
1316 "Nintendo/Contents/registered");
1317 } else if (directory == QStringLiteral("UserNAND")) {
1318 path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
1319 "user/Contents/registered");
1320 } else if (directory == QStringLiteral("SysNAND")) {
1321 path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
1322 "system/Contents/registered");
1323 } else {
1324 path = directory;
1325 }
1326 if (!QFileInfo::exists(path)) {
1327 QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
1328 return;
1329 }
1330 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1331}
1332
1333void GMainWindow::OnGameListAddDirectory() {
1334 const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
1335 if (dir_path.isEmpty())
1336 return;
1337 UISettings::GameDir game_dir{dir_path, false, true};
1338 if (!UISettings::values.game_dirs.contains(game_dir)) {
1339 UISettings::values.game_dirs.append(game_dir);
1340 game_list->PopulateAsync(UISettings::values.game_dirs);
1341 } else {
1342 LOG_WARNING(Frontend, "Selected directory is already in the game list");
1343 }
1344}
1345
1346void GMainWindow::OnGameListShowList(bool show) {
1347 if (emulation_running && ui.action_Single_Window_Mode->isChecked())
1348 return;
1349 game_list->setVisible(show);
1350 game_list_placeholder->setVisible(!show);
1351};
1352
1301void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { 1353void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
1302 u64 title_id{}; 1354 u64 title_id{};
1303 const auto v_file = Core::GetGameFileFromPath(vfs, file); 1355 const auto v_file = Core::GetGameFileFromPath(vfs, file);
@@ -1316,8 +1368,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
1316 1368
1317 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); 1369 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
1318 if (reload) { 1370 if (reload) {
1319 game_list->PopulateAsync(UISettings::values.game_directory_path, 1371 game_list->PopulateAsync(UISettings::values.game_dirs);
1320 UISettings::values.game_directory_deepscan);
1321 } 1372 }
1322 1373
1323 config->Save(); 1374 config->Save();
@@ -1407,8 +1458,7 @@ void GMainWindow::OnMenuInstallToNAND() {
1407 const auto success = [this]() { 1458 const auto success = [this]() {
1408 QMessageBox::information(this, tr("Successfully Installed"), 1459 QMessageBox::information(this, tr("Successfully Installed"),
1409 tr("The file was successfully installed.")); 1460 tr("The file was successfully installed."));
1410 game_list->PopulateAsync(UISettings::values.game_directory_path, 1461 game_list->PopulateAsync(UISettings::values.game_dirs);
1411 UISettings::values.game_directory_deepscan);
1412 FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + 1462 FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
1413 DIR_SEP + "game_list"); 1463 DIR_SEP + "game_list");
1414 }; 1464 };
@@ -1533,14 +1583,6 @@ void GMainWindow::OnMenuInstallToNAND() {
1533 } 1583 }
1534} 1584}
1535 1585
1536void GMainWindow::OnMenuSelectGameListRoot() {
1537 QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
1538 if (!dir_path.isEmpty()) {
1539 UISettings::values.game_directory_path = dir_path;
1540 game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan);
1541 }
1542}
1543
1544void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { 1586void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
1545 const auto res = QMessageBox::information( 1587 const auto res = QMessageBox::information(
1546 this, tr("Changing Emulated Directory"), 1588 this, tr("Changing Emulated Directory"),
@@ -1559,8 +1601,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)
1559 : FileUtil::UserPath::NANDDir, 1601 : FileUtil::UserPath::NANDDir,
1560 dir_path.toStdString()); 1602 dir_path.toStdString());
1561 Service::FileSystem::CreateFactories(*vfs); 1603 Service::FileSystem::CreateFactories(*vfs);
1562 game_list->PopulateAsync(UISettings::values.game_directory_path, 1604 game_list->PopulateAsync(UISettings::values.game_dirs);
1563 UISettings::values.game_directory_deepscan);
1564 } 1605 }
1565} 1606}
1566 1607
@@ -1724,11 +1765,11 @@ void GMainWindow::OnConfigure() {
1724 if (UISettings::values.enable_discord_presence != old_discord_presence) { 1765 if (UISettings::values.enable_discord_presence != old_discord_presence) {
1725 SetDiscordEnabled(UISettings::values.enable_discord_presence); 1766 SetDiscordEnabled(UISettings::values.enable_discord_presence);
1726 } 1767 }
1768 emit UpdateThemedIcons();
1727 1769
1728 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); 1770 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
1729 if (reload) { 1771 if (reload) {
1730 game_list->PopulateAsync(UISettings::values.game_directory_path, 1772 game_list->PopulateAsync(UISettings::values.game_dirs);
1731 UISettings::values.game_directory_deepscan);
1732 } 1773 }
1733 1774
1734 config->Save(); 1775 config->Save();
@@ -1992,8 +2033,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
1992 Service::FileSystem::CreateFactories(*vfs); 2033 Service::FileSystem::CreateFactories(*vfs);
1993 2034
1994 if (behavior == ReinitializeKeyBehavior::Warning) { 2035 if (behavior == ReinitializeKeyBehavior::Warning) {
1995 game_list->PopulateAsync(UISettings::values.game_directory_path, 2036 game_list->PopulateAsync(UISettings::values.game_dirs);
1996 UISettings::values.game_directory_deepscan);
1997 } 2037 }
1998} 2038}
1999 2039
@@ -2158,7 +2198,6 @@ void GMainWindow::UpdateUITheme() {
2158 } 2198 }
2159 2199
2160 QIcon::setThemeSearchPaths(theme_paths); 2200 QIcon::setThemeSearchPaths(theme_paths);
2161 emit UpdateThemedIcons();
2162} 2201}
2163 2202
2164void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { 2203void 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;
30class QLabel; 30class QLabel;
31class WaitTreeWidget; 31class WaitTreeWidget;
32enum class GameListOpenTarget; 32enum class GameListOpenTarget;
33class GameListPlaceholder;
33 34
34namespace Core::Frontend { 35namespace Core::Frontend {
35struct SoftwareKeyboardParameters; 36struct SoftwareKeyboardParameters;
@@ -186,12 +187,13 @@ private slots:
186 void OnGameListCopyTID(u64 program_id); 187 void OnGameListCopyTID(u64 program_id);
187 void OnGameListNavigateToGamedbEntry(u64 program_id, 188 void OnGameListNavigateToGamedbEntry(u64 program_id,
188 const CompatibilityList& compatibility_list); 189 const CompatibilityList& compatibility_list);
190 void OnGameListOpenDirectory(const QString& directory);
191 void OnGameListAddDirectory();
192 void OnGameListShowList(bool show);
189 void OnGameListOpenPerGameProperties(const std::string& file); 193 void OnGameListOpenPerGameProperties(const std::string& file);
190 void OnMenuLoadFile(); 194 void OnMenuLoadFile();
191 void OnMenuLoadFolder(); 195 void OnMenuLoadFolder();
192 void OnMenuInstallToNAND(); 196 void OnMenuInstallToNAND();
193 /// Called whenever a user selects the "File->Select Game List Root" menu item
194 void OnMenuSelectGameListRoot();
195 /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card 197 /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card
196 void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); 198 void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target);
197 void OnMenuRecentFile(); 199 void OnMenuRecentFile();
@@ -223,6 +225,8 @@ private:
223 GameList* game_list; 225 GameList* game_list;
224 LoadingScreen* loading_screen; 226 LoadingScreen* loading_screen;
225 227
228 GameListPlaceholder* game_list_placeholder;
229
226 // Status bar elements 230 // Status bar elements
227 QLabel* message_label = nullptr; 231 QLabel* message_label = nullptr;
228 QLabel* emu_speed_label = nullptr; 232 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 @@
62 <addaction name="action_Load_File"/> 62 <addaction name="action_Load_File"/>
63 <addaction name="action_Load_Folder"/> 63 <addaction name="action_Load_Folder"/>
64 <addaction name="separator"/> 64 <addaction name="separator"/>
65 <addaction name="action_Select_Game_List_Root"/>
66 <addaction name="menu_recent_files"/> 65 <addaction name="menu_recent_files"/>
67 <addaction name="separator"/> 66 <addaction name="separator"/>
68 <addaction name="action_Select_NAND_Directory"/> 67 <addaction name="action_Select_NAND_Directory"/>
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 @@
8#include <atomic> 8#include <atomic>
9#include <vector> 9#include <vector>
10#include <QByteArray> 10#include <QByteArray>
11#include <QMetaType>
11#include <QString> 12#include <QString>
12#include <QStringList> 13#include <QStringList>
14#include <QVector>
13#include "common/common_types.h" 15#include "common/common_types.h"
14 16
15namespace UISettings { 17namespace UISettings {
@@ -25,6 +27,18 @@ struct Shortcut {
25using Themes = std::array<std::pair<const char*, const char*>, 2>; 27using Themes = std::array<std::pair<const char*, const char*>, 2>;
26extern const Themes themes; 28extern const Themes themes;
27 29
30struct GameDir {
31 QString path;
32 bool deep_scan;
33 bool expanded;
34 bool operator==(const GameDir& rhs) const {
35 return path == rhs.path;
36 };
37 bool operator!=(const GameDir& rhs) const {
38 return !operator==(rhs);
39 };
40};
41
28struct Values { 42struct Values {
29 QByteArray geometry; 43 QByteArray geometry;
30 QByteArray state; 44 QByteArray state;
@@ -55,8 +69,9 @@ struct Values {
55 QString roms_path; 69 QString roms_path;
56 QString symbols_path; 70 QString symbols_path;
57 QString screenshot_path; 71 QString screenshot_path;
58 QString game_directory_path; 72 QString game_dir_deprecated;
59 bool game_directory_deepscan; 73 bool game_dir_deprecated_deepscan;
74 QVector<UISettings::GameDir> game_dirs;
60 QStringList recent_files; 75 QStringList recent_files;
61 76
62 QString theme; 77 QString theme;
@@ -84,3 +99,5 @@ struct Values {
84 99
85extern Values values; 100extern Values values;
86} // namespace UISettings 101} // namespace UISettings
102
103Q_DECLARE_METATYPE(UISettings::GameDir*);