summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar fearlessTobi2019-05-01 23:21:04 +0200
committerGravatar FearlessTobi2019-09-04 16:47:32 +0200
commit2d8eba5bafd7fe9da00c8a57c605a503c3ece478 (patch)
treec3bc64c33ab43f6bd0d7bb6f4ec63b11490a41e8 /src
parentAdd assets and licenses (diff)
downloadyuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.gz
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.xz
yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.zip
yuzu: Add support for multiple game directories
Ported from https://github.com/citra-emu/citra/pull/3617.
Diffstat (limited to 'src')
-rw-r--r--src/yuzu/configuration/config.cpp42
-rw-r--r--src/yuzu/configuration/configure_general.cpp5
-rw-r--r--src/yuzu/configuration/configure_general.ui7
-rw-r--r--src/yuzu/game_list.cpp430
-rw-r--r--src/yuzu/game_list.h44
-rw-r--r--src/yuzu/game_list_p.h111
-rw-r--r--src/yuzu/game_list_worker.cpp83
-rw-r--r--src/yuzu/game_list_worker.h25
-rw-r--r--src/yuzu/main.cpp85
-rw-r--r--src/yuzu/main.h8
-rw-r--r--src/yuzu/main.ui1
-rw-r--r--src/yuzu/uisettings.h20
12 files changed, 666 insertions, 195 deletions
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 0456248ac..f2f116a87 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -517,10 +517,35 @@ void Config::ReadPathValues() {
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 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("INSTALLED");
539 game_dir.expanded = true;
540 UISettings::values.game_dirs.append(game_dir);
541 game_dir.path = QStringLiteral("SYSTEM");
542 UISettings::values.game_dirs.append(game_dir);
543 if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) {
544 game_dir.path = UISettings::values.game_dir_deprecated;
545 game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan;
546 UISettings::values.game_dirs.append(game_dir);
547 }
548 }
524 UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); 549 UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
525 550
526 qt_config->endGroup(); 551 qt_config->endGroup();
@@ -899,10 +924,15 @@ void Config::SavePathValues() {
899 WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); 924 WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);
900 WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); 925 WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);
901 WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); 926 WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path);
902 WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, 927 qt_config->beginWriteArray(QStringLiteral("gamedirs"));
903 QStringLiteral(".")); 928 for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
904 WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, 929 qt_config->setArrayIndex(i);
905 false); 930 const auto& game_dir = UISettings::values.game_dirs.at(i);
931 WriteSetting(QStringLiteral("path"), game_dir.path);
932 WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false);
933 WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
934 }
935 qt_config->endArray();
906 WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); 936 WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);
907 937
908 qt_config->endGroup(); 938 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..65947c59b 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
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() {
87 QStandardItem* folder;
88 QStandardItem* child;
89 QString file_path;
90 int folder_count = item_model->rowCount();
91 for (int i = 0; i < folder_count; ++i) {
92 folder = item_model->item(i, 0);
93 QModelIndex folder_index = folder->index();
94 int childrenCount = folder->rowCount();
95 for (int j = 0; j < childrenCount; ++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,112 @@ 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 GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>();
164 if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir ||
165 type == GameListItemType::SystemDir)
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 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 childrenTotal = 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 QModelIndex folder_index = folder->index();
185 int childrenCount = folder->rowCount();
186 for (int j = 0; j < childrenCount; ++j) {
187 ++childrenTotal;
188 tree_view->setRowHidden(j, folder_index, false);
189 }
161 } 190 }
162 search_field->setFilterResult(row_count, row_count); 191 search_field->setFilterResult(childrenTotal, childrenTotal);
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 QModelIndex folder_index = folder->index();
168 child_file->data(GameListItemPath::FullPathRole).toString().toLower(); 197 int childrenCount = folder->rowCount();
169 const QString file_title = 198 for (int j = 0; j < childrenCount; ++j) {
170 child_file->data(GameListItemPath::TitleRole).toString().toLower(); 199 ++childrenTotal;
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, childrenTotal);
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 int icon_size = UISettings::values.icon_size;
234 switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
235 case GameListItemType::InstalledDir:
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::SystemDir:
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::CustomDir: {
250 const UISettings::GameDir* game_dir =
251 child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
252 QString icon_name = QFileInfo::exists(game_dir->path) ? QStringLiteral("folder")
253 : QStringLiteral("bad_folder");
254 child->setData(
255 QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
256 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
257 Qt::DecorationRole);
258 break;
259 }
260 case GameListItemType::AddDir:
261 child->setData(
262 QIcon::fromTheme(QStringLiteral("plus"))
263 .pixmap(icon_size)
264 .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
265 Qt::DecorationRole);
266 break;
189 } 267 }
190 } 268 }
191} 269}
@@ -230,12 +308,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
230 item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); 308 item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type"));
231 item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); 309 item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size"));
232 } 310 }
311 item_model->setSortRole(GameListItemPath::TitleRole);
233 312
313 connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);
234 connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); 314 connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
235 connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); 315 connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
316 connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
317 connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
236 318
237 // We must register all custom types with the Qt Automoc system so that we are able to use it 319 // 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. 320 // it with signals/slots. In this case, QList falls under the umbrells of custom types.
239 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); 321 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
240 322
241 layout->setContentsMargins(0, 0, 0, 0); 323 layout->setContentsMargins(0, 0, 0, 0);
@@ -263,38 +345,67 @@ void GameList::clearFilter() {
263 search_field->clear(); 345 search_field->clear();
264} 346}
265 347
266void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { 348void GameList::AddDirEntry(GameListDir* entry_items) {
267 item_model->invisibleRootItem()->appendRow(entry_items); 349 item_model->invisibleRootItem()->appendRow(entry_items);
350 tree_view->setExpanded(
351 entry_items->index(),
352 entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
268} 353}
269 354
270void GameList::ValidateEntry(const QModelIndex& item) { 355void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
271 // We don't care about the individual QStandardItem that was selected, but its row. 356 parent->appendRow(entry_items);
272 const int row = item_model->itemFromIndex(item)->row(); 357}
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 358
282 const QFileInfo file_info{file_path}; 359void GameList::ValidateEntry(const QModelIndex& item) {
283 if (file_info.isDir()) { 360 auto selected = item.sibling(item.row(), 0);
284 const QDir dir{file_path}; 361
285 const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); 362 switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
286 if (matching_main.size() == 1) { 363 case GameListItemType::Game: {
287 emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); 364 QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
365 if (file_path.isEmpty())
366 return;
367 QFileInfo file_info(file_path);
368 if (!file_info.exists())
369 return;
370
371 if (file_info.isDir()) {
372 const QDir dir{file_path};
373 const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
374 if (matching_main.size() == 1) {
375 emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
376 }
377 return;
288 } 378 }
289 return; 379
380 // Users usually want to run a different game after closing one
381 search_field->clear();
382 emit GameChosen(file_path);
383 break;
290 } 384 }
385 case GameListItemType::AddDir:
386 emit AddDirectory();
387 break;
388 }
389}
291 390
292 // Users usually want to run a diffrent game after closing one 391bool GameList::isEmpty() {
293 search_field->clear(); 392 for (int i = 0; i < item_model->rowCount(); i++) {
294 emit GameChosen(file_path); 393 const QStandardItem* child = item_model->invisibleRootItem()->child(i);
394 GameListItemType type = static_cast<GameListItemType>(child->type());
395 if (!child->hasChildren() &&
396 (type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) {
397 item_model->invisibleRootItem()->removeRow(child->row());
398 i--;
399 };
400 }
401 return !item_model->invisibleRootItem()->hasChildren();
295} 402}
296 403
297void GameList::DonePopulating(QStringList watch_list) { 404void GameList::DonePopulating(QStringList watch_list) {
405 emit ShowList(!isEmpty());
406
407 item_model->invisibleRootItem()->appendRow(new GameListAddDir());
408
298 // Clear out the old directories to watch for changes and add the new ones 409 // Clear out the old directories to watch for changes and add the new ones
299 auto watch_dirs = watcher->directories(); 410 auto watch_dirs = watcher->directories();
300 if (!watch_dirs.isEmpty()) { 411 if (!watch_dirs.isEmpty()) {
@@ -311,9 +422,16 @@ void GameList::DonePopulating(QStringList watch_list) {
311 QCoreApplication::processEvents(); 422 QCoreApplication::processEvents();
312 } 423 }
313 tree_view->setEnabled(true); 424 tree_view->setEnabled(true);
314 int rowCount = tree_view->model()->rowCount(); 425 int folder_count = tree_view->model()->rowCount();
315 search_field->setFilterResult(rowCount, rowCount); 426 int childrenTotal = 0;
316 if (rowCount > 0) { 427 for (int i = 0; i < folder_count; ++i) {
428 int childrenCount = item_model->item(i, 0)->rowCount();
429 for (int j = 0; j < childrenCount; ++j) {
430 ++childrenTotal;
431 }
432 }
433 search_field->setFilterResult(childrenTotal, childrenTotal);
434 if (childrenTotal > 0) {
317 search_field->setFocus(); 435 search_field->setFocus();
318 } 436 }
319} 437}
@@ -323,12 +441,26 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
323 if (!item.isValid()) 441 if (!item.isValid())
324 return; 442 return;
325 443
326 int row = item_model->itemFromIndex(item)->row(); 444 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; 445 QMenu context_menu;
446 switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
447 case GameListItemType::Game:
448 AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
449 selected.data(GameListItemPath::FullPathRole).toString().toStdString());
450 break;
451 case GameListItemType::CustomDir:
452 AddPermDirPopup(context_menu, selected);
453 AddCustomDirPopup(context_menu, selected);
454 break;
455 case GameListItemType::InstalledDir:
456 case GameListItemType::SystemDir:
457 AddPermDirPopup(context_menu, selected);
458 break;
459 }
460 context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
461}
462
463void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) {
332 QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); 464 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")); 465 QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));
334 QAction* open_transferable_shader_cache = 466 QAction* open_transferable_shader_cache =
@@ -344,19 +476,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
344 auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); 476 auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
345 navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); 477 navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
346 478
347 connect(open_save_location, &QAction::triggered, 479 connect(open_save_location, &QAction::triggered, [this, program_id]() {
348 [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); 480 emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData);
349 connect(open_lfs_location, &QAction::triggered, 481 });
350 [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); 482 connect(open_lfs_location, &QAction::triggered, [this, program_id]() {
483 emit OpenFolderRequested(program_id, GameListOpenTarget::ModData);
484 });
351 connect(open_transferable_shader_cache, &QAction::triggered, 485 connect(open_transferable_shader_cache, &QAction::triggered,
352 [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); 486 [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
353 connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); 487 connect(dump_romfs, &QAction::triggered,
354 connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); 488 [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); });
355 connect(navigate_to_gamedb_entry, &QAction::triggered, 489 connect(copy_tid, &QAction::triggered,
356 [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); 490 [this, program_id]() { emit CopyTIDRequested(program_id); });
357 connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); 491 connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
492 emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
493 });
494 connect(properties, &QAction::triggered,
495 [this, path]() { emit OpenPerGameGeneralRequested(path); });
496};
497
498void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
499 UISettings::GameDir& game_dir =
500 *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
501
502 QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
503 QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
504
505 deep_scan->setCheckable(true);
506 deep_scan->setChecked(game_dir.deep_scan);
507
508 connect(deep_scan, &QAction::triggered, [this, &game_dir] {
509 game_dir.deep_scan = !game_dir.deep_scan;
510 PopulateAsync(UISettings::values.game_dirs);
511 });
512 connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] {
513 UISettings::values.game_dirs.removeOne(game_dir);
514 item_model->invisibleRootItem()->removeRow(selected.row());
515 });
516}
358 517
359 context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); 518void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
519 UISettings::GameDir& game_dir =
520 *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
521
522 QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up"));
523 QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down "));
524 QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location"));
525
526 int row = selected.row();
527
528 move_up->setEnabled(row > 0);
529 move_down->setEnabled(row < item_model->rowCount() - 2);
530
531 connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] {
532 // find the indices of the items in settings and swap them
533 UISettings::values.game_dirs.swap(
534 UISettings::values.game_dirs.indexOf(game_dir),
535 UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() - 1, 0)
536 .data(GameListDir::GameDirRole)
537 .value<UISettings::GameDir*>()));
538 // move the treeview items
539 QList<QStandardItem*> item = item_model->takeRow(row);
540 item_model->invisibleRootItem()->insertRow(row - 1, item);
541 tree_view->setExpanded(selected, game_dir.expanded);
542 });
543
544 connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] {
545 // find the indices of the items in settings and swap them
546 UISettings::values.game_dirs.swap(
547 UISettings::values.game_dirs.indexOf(game_dir),
548 UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() + 1, 0)
549 .data(GameListDir::GameDirRole)
550 .value<UISettings::GameDir*>()));
551 // move the treeview items
552 QList<QStandardItem*> item = item_model->takeRow(row);
553 item_model->invisibleRootItem()->insertRow(row + 1, item);
554 tree_view->setExpanded(selected, game_dir.expanded);
555 });
556
557 connect(open_directory_location, &QAction::triggered,
558 [this, game_dir] { emit OpenDirectory(game_dir.path); });
360} 559}
361 560
362void GameList::LoadCompatibilityList() { 561void GameList::LoadCompatibilityList() {
@@ -403,14 +602,7 @@ void GameList::LoadCompatibilityList() {
403 } 602 }
404} 603}
405 604
406void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { 605void GameList::PopulateAsync(QList<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); 606 tree_view->setEnabled(false);
415 607
416 // Update the columns in case UISettings has changed 608 // Update the columns in case UISettings has changed
@@ -433,17 +625,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
433 625
434 // Delete any rows that might already exist if we're repopulating 626 // Delete any rows that might already exist if we're repopulating
435 item_model->removeRows(0, item_model->rowCount()); 627 item_model->removeRows(0, item_model->rowCount());
628 search_field->clear();
436 629
437 emit ShouldCancelWorker(); 630 emit ShouldCancelWorker();
438 631
439 GameListWorker* worker = 632 GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);
440 new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
441 633
442 connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); 634 connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
635 connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
636 Qt::QueuedConnection);
443 connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, 637 connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
444 Qt::QueuedConnection); 638 Qt::QueuedConnection);
445 // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel 639 // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
446 // without delay. 640 // cancel without delay.
447 connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, 641 connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
448 Qt::DirectConnection); 642 Qt::DirectConnection);
449 643
@@ -471,10 +665,42 @@ const QStringList GameList::supported_file_extensions = {
471 QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; 665 QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
472 666
473void GameList::RefreshGameDirectory() { 667void GameList::RefreshGameDirectory() {
474 if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { 668 if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
475 LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); 669 LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
476 search_field->clear(); 670 PopulateAsync(UISettings::values.game_dirs);
477 PopulateAsync(UISettings::values.game_directory_path,
478 UISettings::values.game_directory_deepscan);
479 } 671 }
480} 672}
673
674GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
675 this->main_window = parent;
676
677 connect(main_window, &GMainWindow::UpdateThemedIcons, this,
678 &GameListPlaceholder::onUpdateThemedIcons);
679
680 layout = new QVBoxLayout;
681 image = new QLabel;
682 text = new QLabel;
683 layout->setAlignment(Qt::AlignCenter);
684 image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
685
686 text->setText(tr("Double-click to add a new folder to the game list "));
687 QFont font = text->font();
688 font.setPointSize(20);
689 text->setFont(font);
690 text->setAlignment(Qt::AlignHCenter);
691 image->setAlignment(Qt::AlignHCenter);
692
693 layout->addWidget(image);
694 layout->addWidget(text);
695 setLayout(layout);
696}
697
698GameListPlaceholder::~GameListPlaceholder() = default;
699
700void GameListPlaceholder::onUpdateThemedIcons() {
701 image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
702}
703
704void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
705 emit GameListPlaceholder::AddDirectory();
706}
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index f8f8bd6c5..a2b58aba5 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -19,10 +19,14 @@
19#include <QWidget> 19#include <QWidget>
20 20
21#include "common/common_types.h" 21#include "common/common_types.h"
22#include "ui_settings.h"
22#include "yuzu/compatibility_list.h" 23#include "yuzu/compatibility_list.h"
23 24
24class GameListWorker; 25class GameListWorker;
25class GameListSearchField; 26class GameListSearchField;
27template <typename>
28class QList;
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();
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();
58 64
59 void LoadCompatibilityList(); 65 void LoadCompatibilityList();
60 void PopulateAsync(const QString& dir_path, bool deep_scan); 66 void PopulateAsync(QList<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(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,25 @@ 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 GMainWindow* main_window = nullptr;
139 QVBoxLayout* layout = nullptr;
140 QLabel* image = nullptr;
141 QLabel* text = nullptr;
142};
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index ece534dd6..f5abb759d 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -10,6 +10,7 @@
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,16 @@
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 InstalledDir = QStandardItem::UserType + 3,
30 SystemDir = QStandardItem::UserType + 4,
31 AddDir = QStandardItem::UserType + 5
32};
33
34Q_DECLARE_METATYPE(GameListItemType);
35
25/** 36/**
26 * Gets the default icon (for games without valid title metadata) 37 * Gets the default icon (for games without valid title metadata)
27 * @param size The desired width and height of the default icon. 38 * @param size The desired width and height of the default icon.
@@ -36,8 +47,13 @@ static QPixmap GetDefaultIcon(u32 size) {
36class GameListItem : public QStandardItem { 47class GameListItem : public QStandardItem {
37 48
38public: 49public:
50 // used to access type from item index
51 static const int TypeRole = Qt::UserRole + 1;
52 static const int SortRole = Qt::UserRole + 2;
39 GameListItem() = default; 53 GameListItem() = default;
40 explicit GameListItem(const QString& string) : QStandardItem(string) {} 54 GameListItem(const QString& string) : QStandardItem(string) {
55 setData(string, SortRole);
56 }
41}; 57};
42 58
43/** 59/**
@@ -48,14 +64,15 @@ public:
48 */ 64 */
49class GameListItemPath : public GameListItem { 65class GameListItemPath : public GameListItem {
50public: 66public:
51 static const int FullPathRole = Qt::UserRole + 1; 67 static const int TitleRole = SortRole;
52 static const int TitleRole = Qt::UserRole + 2; 68 static const int FullPathRole = SortRole + 1;
53 static const int ProgramIdRole = Qt::UserRole + 3; 69 static const int ProgramIdRole = SortRole + 2;
54 static const int FileTypeRole = Qt::UserRole + 4; 70 static const int FileTypeRole = SortRole + 3;
55 71
56 GameListItemPath() = default; 72 GameListItemPath() = default;
57 GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, 73 GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
58 const QString& game_name, const QString& game_type, u64 program_id) { 74 const QString& game_name, const QString& game_type, u64 program_id) {
75 setData(type(), TypeRole);
59 setData(game_path, FullPathRole); 76 setData(game_path, FullPathRole);
60 setData(game_name, TitleRole); 77 setData(game_name, TitleRole);
61 setData(qulonglong(program_id), ProgramIdRole); 78 setData(qulonglong(program_id), ProgramIdRole);
@@ -72,6 +89,10 @@ public:
72 setData(picture, Qt::DecorationRole); 89 setData(picture, Qt::DecorationRole);
73 } 90 }
74 91
92 int type() const override {
93 return static_cast<int>(GameListItemType::Game);
94 }
95
75 QVariant data(int role) const override { 96 QVariant data(int role) const override {
76 if (role == Qt::DisplayRole) { 97 if (role == Qt::DisplayRole) {
77 std::string filename; 98 std::string filename;
@@ -103,9 +124,11 @@ public:
103class GameListItemCompat : public GameListItem { 124class GameListItemCompat : public GameListItem {
104 Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) 125 Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
105public: 126public:
106 static const int CompatNumberRole = Qt::UserRole + 1; 127 static const int CompatNumberRole = SortRole;
107 GameListItemCompat() = default; 128 GameListItemCompat() = default;
108 explicit GameListItemCompat(const QString& compatibility) { 129 explicit GameListItemCompat(const QString& compatibility) {
130 setData(type(), TypeRole);
131
109 struct CompatStatus { 132 struct CompatStatus {
110 QString color; 133 QString color;
111 const char* text; 134 const char* text;
@@ -135,6 +158,10 @@ public:
135 setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); 158 setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
136 } 159 }
137 160
161 int type() const override {
162 return static_cast<int>(GameListItemType::Game);
163 }
164
138 bool operator<(const QStandardItem& other) const override { 165 bool operator<(const QStandardItem& other) const override {
139 return data(CompatNumberRole) < other.data(CompatNumberRole); 166 return data(CompatNumberRole) < other.data(CompatNumberRole);
140 } 167 }
@@ -146,12 +173,12 @@ public:
146 * human-readable string representation will be displayed to the user. 173 * human-readable string representation will be displayed to the user.
147 */ 174 */
148class GameListItemSize : public GameListItem { 175class GameListItemSize : public GameListItem {
149
150public: 176public:
151 static const int SizeRole = Qt::UserRole + 1; 177 static const int SizeRole = SortRole;
152 178
153 GameListItemSize() = default; 179 GameListItemSize() = default;
154 explicit GameListItemSize(const qulonglong size_bytes) { 180 explicit GameListItemSize(const qulonglong size_bytes) {
181 setData(type(), TypeRole);
155 setData(size_bytes, SizeRole); 182 setData(size_bytes, SizeRole);
156 } 183 }
157 184
@@ -167,6 +194,10 @@ public:
167 } 194 }
168 } 195 }
169 196
197 int type() const override {
198 return static_cast<int>(GameListItemType::Game);
199 }
200
170 /** 201 /**
171 * This operator is, in practice, only used by the TreeView sorting systems. 202 * 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 203 * Override it so that it will correctly sort by numerical value instead of by string
@@ -177,6 +208,67 @@ public:
177 } 208 }
178}; 209};
179 210
211class GameListDir : public GameListItem {
212public:
213 static const int GameDirRole = Qt::UserRole + 2;
214
215 explicit GameListDir(UISettings::GameDir& directory,
216 GameListItemType dir_type = GameListItemType::CustomDir)
217 : dir_type{dir_type} {
218 setData(type(), TypeRole);
219
220 UISettings::GameDir* game_dir = &directory;
221 setData(QVariant::fromValue(game_dir), GameDirRole);
222
223 int icon_size = UISettings::values.icon_size;
224 switch (dir_type) {
225 case GameListItemType::InstalledDir:
226 setData(QIcon::fromTheme("sd_card").pixmap(icon_size).scaled(
227 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
228 Qt::DecorationRole);
229 setData("Installed Titles", Qt::DisplayRole);
230 break;
231 case GameListItemType::SystemDir:
232 setData(QIcon::fromTheme("chip").pixmap(icon_size).scaled(
233 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
234 Qt::DecorationRole);
235 setData("System Titles", Qt::DisplayRole);
236 break;
237 case GameListItemType::CustomDir:
238 QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
239 setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
240 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
241 Qt::DecorationRole);
242 setData(game_dir->path, Qt::DisplayRole);
243 break;
244 };
245 };
246
247 int type() const override {
248 return static_cast<int>(dir_type);
249 }
250
251private:
252 GameListItemType dir_type;
253};
254
255class GameListAddDir : public GameListItem {
256public:
257 explicit GameListAddDir() {
258 setData(type(), TypeRole);
259
260 int icon_size = UISettings::values.icon_size;
261 setData(QIcon::fromTheme("plus").pixmap(icon_size).scaled(
262 icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
263 Qt::DecorationRole);
264 setData("Add New Game Directory", Qt::DisplayRole);
265 }
266
267 int type() const override {
268 return static_cast<int>(GameListItemType::AddDir);
269 }
270};
271
180class GameList; 272class GameList;
181class QHBoxLayout; 273class QHBoxLayout;
182class QTreeView; 274class QTreeView;
@@ -195,6 +287,9 @@ public:
195 void clear(); 287 void clear();
196 void setFocus(); 288 void setFocus();
197 289
290 int visible;
291 int total;
292
198private: 293private:
199 class KeyReleaseEater : public QObject { 294 class KeyReleaseEater : public QObject {
200 public: 295 public:
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index 77f358630..8c6621c98 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -223,21 +223,38 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
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 QList<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 if (parent_dir->type() == static_cast<int>(GameListItemType::InstalledDir)) {
244 installed_games = cache.ListEntriesFilterOrigin(
245 ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
246 auto installed_sdmc_games = cache.ListEntriesFilterOrigin(
247 ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
248
249 installed_games.insert(installed_games.end(), installed_sdmc_games.begin(),
250 installed_sdmc_games.end());
251 } else if (parent_dir->type() == static_cast<int>(GameListItemType::SystemDir)) {
252 installed_games = cache.ListEntriesFilterOrigin(
253 ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
254 }
238 255
239 for (const auto& [slot, game] : installed_games) { 256 for (const auto& [slot, game] : installed_games) {
240 if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) 257 if (slot == ContentProviderUnionSlot::FrontendManual)
241 continue; 258 continue;
242 259
243 const auto file = cache.GetEntryUnparsed(game.title_id, game.type); 260 const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
@@ -250,21 +267,22 @@ void GameListWorker::AddTitlesToGameList() {
250 u64 program_id = 0; 267 u64 program_id = 0;
251 loader->ReadProgramId(program_id); 268 loader->ReadProgramId(program_id);
252 269
253 const FileSys::PatchManager patch{program_id}; 270 const PatchManager patch{program_id};
254 const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); 271 const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
255 if (control != nullptr) 272 if (control != nullptr)
256 GetMetadataFromControlNCA(patch, *control, icon, name); 273 GetMetadataFromControlNCA(patch, *control, icon, name);
257 274
258 emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, 275 emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
259 compatibility_list, patch)); 276 compatibility_list, patch),
277 parent_dir);
260 } 278 }
261} 279}
262 280
263void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, 281void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
264 unsigned int recursion) { 282 unsigned int recursion, GameListDir* parent_dir) {
265 const auto callback = [this, target, recursion](u64* num_entries_out, 283 const auto callback = [this, target, recursion,
266 const std::string& directory, 284 parent_dir](u64* num_entries_out, const std::string& directory,
267 const std::string& virtual_name) -> bool { 285 const std::string& virtual_name) -> bool {
268 if (stop_processing) { 286 if (stop_processing) {
269 // Breaks the callback loop. 287 // Breaks the callback loop.
270 return false; 288 return false;
@@ -317,11 +335,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
317 const FileSys::PatchManager patch{program_id}; 335 const FileSys::PatchManager patch{program_id};
318 336
319 emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, 337 emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
320 compatibility_list, patch)); 338 compatibility_list, patch),
339 parent_dir);
321 } 340 }
322 } else if (is_dir && recursion > 0) { 341 } else if (is_dir && recursion > 0) {
323 watch_list.append(QString::fromStdString(physical_name)); 342 watch_list.append(QString::fromStdString(physical_name));
324 ScanFileSystem(target, physical_name, recursion - 1); 343 ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
325 } 344 }
326 345
327 return true; 346 return true;
@@ -332,12 +351,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
332 351
333void GameListWorker::run() { 352void GameListWorker::run() {
334 stop_processing = false; 353 stop_processing = false;
335 watch_list.append(dir_path); 354
336 provider->ClearAllEntries(); 355 for (UISettings::GameDir& game_dir : game_dirs) {
337 ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), 356 if (game_dir.path == "INSTALLED") {
338 deep_scan ? 256 : 0); 357 GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
339 AddTitlesToGameList(); 358 emit DirEntryReady({game_list_dir});
340 ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); 359 AddTitlesToGameList(game_list_dir);
360 } else if (game_dir.path == "SYSTEM") {
361 GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
362 emit DirEntryReady({game_list_dir});
363 AddTitlesToGameList(game_list_dir);
364 } else {
365 watch_list.append(game_dir.path);
366 GameListDir* game_list_dir = new GameListDir(game_dir);
367 emit DirEntryReady({game_list_dir});
368 provider->ClearAllEntries();
369 ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2,
370 game_list_dir);
371 ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
372 game_dir.deep_scan ? 256 : 0, game_list_dir);
373 }
374 };
375
341 emit Finished(watch_list); 376 emit Finished(watch_list);
342} 377}
343 378
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h
index 7c3074af9..46ec96516 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -33,9 +33,10 @@ class GameListWorker : public QObject, public QRunnable {
33 Q_OBJECT 33 Q_OBJECT
34 34
35public: 35public:
36 GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, 36 explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
37 FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, 37 FileSys::ManualContentProvider* provider,
38 const CompatibilityList& compatibility_list); 38 QList<UISettings::GameDir>& game_dirs,
39 const CompatibilityList& compatibility_list);
39 ~GameListWorker() override; 40 ~GameListWorker() override;
40 41
41 /// Starts the processing of directory tree information. 42 /// Starts the processing of directory tree information.
@@ -48,31 +49,33 @@ signals:
48 /** 49 /**
49 * The `EntryReady` signal is emitted once an entry has been prepared and is ready 50 * The `EntryReady` signal is emitted once an entry has been prepared and is ready
50 * to be added to the game list. 51 * 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. 52 * @param entry_items a list with `QStandardItem`s that make up the columns of the new
53 * entry.
52 */ 54 */
53 void EntryReady(QList<QStandardItem*> entry_items); 55 void DirEntryReady(GameListDir* entry_items);
56 void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
54 57
55 /** 58 /**
56 * After the worker has traversed the game directory looking for entries, this signal is emitted 59 * 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. 60 * emitted with a list of folders that should be watched for changes as well.
58 */ 61 */
59 void Finished(QStringList watch_list); 62 void Finished(QStringList watch_list);
60 63
61private: 64private:
62 void AddTitlesToGameList(); 65 void AddTitlesToGameList(GameListDir* parent_dir);
63 66
64 enum class ScanTarget { 67 enum class ScanTarget {
65 FillManualContentProvider, 68 FillManualContentProvider,
66 PopulateGameList, 69 PopulateGameList,
67 }; 70 };
68 71
69 void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); 72 void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion,
73 GameListDir* parent_dir);
70 74
71 std::shared_ptr<FileSys::VfsFilesystem> vfs; 75 std::shared_ptr<FileSys::VfsFilesystem> vfs;
72 FileSys::ManualContentProvider* provider; 76 FileSys::ManualContentProvider* provider;
73 QStringList watch_list; 77 QStringList watch_list;
74 QString dir_path;
75 bool deep_scan;
76 const CompatibilityList& compatibility_list; 78 const CompatibilityList& compatibility_list;
79 QList<UISettings::GameDir>& game_dirs;
77 std::atomic_bool stop_processing; 80 std::atomic_bool stop_processing;
78}; 81};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index ac57229d5..b2de9545b 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -216,8 +216,7 @@ GMainWindow::GMainWindow()
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,45 @@ 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(QString directory) {
1313 QString path;
1314 if (directory == QStringLiteral("INSTALLED")) {
1315 // TODO: Find a better solution when installing files to the SD card gets implemented
1316 path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() +
1317 std::string("user/Contents/registered"));
1318 } else if (directory == QStringLiteral("SYSTEM")) {
1319 path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() +
1320 std::string("system/Contents/registered"));
1321 } else {
1322 path = directory;
1323 }
1324 if (!QFileInfo::exists(path)) {
1325 QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
1326 return;
1327 }
1328 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1329}
1330
1331void GMainWindow::OnGameListAddDirectory() {
1332 QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
1333 if (dir_path.isEmpty())
1334 return;
1335 UISettings::GameDir game_dir{dir_path, false, true};
1336 if (!UISettings::values.game_dirs.contains(game_dir)) {
1337 UISettings::values.game_dirs.append(game_dir);
1338 game_list->PopulateAsync(UISettings::values.game_dirs);
1339 } else {
1340 LOG_WARNING(Frontend, "Selected directory is already in the game list");
1341 }
1342}
1343
1344void GMainWindow::OnGameListShowList(bool show) {
1345 if (emulation_running && ui.action_Single_Window_Mode->isChecked())
1346 return;
1347 game_list->setVisible(show);
1348 game_list_placeholder->setVisible(!show);
1349};
1350
1301void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { 1351void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
1302 u64 title_id{}; 1352 u64 title_id{};
1303 const auto v_file = Core::GetGameFileFromPath(vfs, file); 1353 const auto v_file = Core::GetGameFileFromPath(vfs, file);
@@ -1316,8 +1366,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
1316 1366
1317 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); 1367 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
1318 if (reload) { 1368 if (reload) {
1319 game_list->PopulateAsync(UISettings::values.game_directory_path, 1369 game_list->PopulateAsync(UISettings::values.game_dirs);
1320 UISettings::values.game_directory_deepscan);
1321 } 1370 }
1322 1371
1323 config->Save(); 1372 config->Save();
@@ -1407,8 +1456,7 @@ void GMainWindow::OnMenuInstallToNAND() {
1407 const auto success = [this]() { 1456 const auto success = [this]() {
1408 QMessageBox::information(this, tr("Successfully Installed"), 1457 QMessageBox::information(this, tr("Successfully Installed"),
1409 tr("The file was successfully installed.")); 1458 tr("The file was successfully installed."));
1410 game_list->PopulateAsync(UISettings::values.game_directory_path, 1459 game_list->PopulateAsync(UISettings::values.game_dirs);
1411 UISettings::values.game_directory_deepscan);
1412 FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + 1460 FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
1413 DIR_SEP + "game_list"); 1461 DIR_SEP + "game_list");
1414 }; 1462 };
@@ -1533,14 +1581,6 @@ void GMainWindow::OnMenuInstallToNAND() {
1533 } 1581 }
1534} 1582}
1535 1583
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) { 1584void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
1545 const auto res = QMessageBox::information( 1585 const auto res = QMessageBox::information(
1546 this, tr("Changing Emulated Directory"), 1586 this, tr("Changing Emulated Directory"),
@@ -1559,8 +1599,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)
1559 : FileUtil::UserPath::NANDDir, 1599 : FileUtil::UserPath::NANDDir,
1560 dir_path.toStdString()); 1600 dir_path.toStdString());
1561 Service::FileSystem::CreateFactories(*vfs); 1601 Service::FileSystem::CreateFactories(*vfs);
1562 game_list->PopulateAsync(UISettings::values.game_directory_path, 1602 game_list->PopulateAsync(UISettings::values.game_dirs);
1563 UISettings::values.game_directory_deepscan);
1564 } 1603 }
1565} 1604}
1566 1605
@@ -1724,11 +1763,11 @@ void GMainWindow::OnConfigure() {
1724 if (UISettings::values.enable_discord_presence != old_discord_presence) { 1763 if (UISettings::values.enable_discord_presence != old_discord_presence) {
1725 SetDiscordEnabled(UISettings::values.enable_discord_presence); 1764 SetDiscordEnabled(UISettings::values.enable_discord_presence);
1726 } 1765 }
1766 emit UpdateThemedIcons();
1727 1767
1728 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); 1768 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
1729 if (reload) { 1769 if (reload) {
1730 game_list->PopulateAsync(UISettings::values.game_directory_path, 1770 game_list->PopulateAsync(UISettings::values.game_dirs);
1731 UISettings::values.game_directory_deepscan);
1732 } 1771 }
1733 1772
1734 config->Save(); 1773 config->Save();
@@ -1992,8 +2031,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
1992 Service::FileSystem::CreateFactories(*vfs); 2031 Service::FileSystem::CreateFactories(*vfs);
1993 2032
1994 if (behavior == ReinitializeKeyBehavior::Warning) { 2033 if (behavior == ReinitializeKeyBehavior::Warning) {
1995 game_list->PopulateAsync(UISettings::values.game_directory_path, 2034 game_list->PopulateAsync(UISettings::values.game_dirs);
1996 UISettings::values.game_directory_deepscan);
1997 } 2035 }
1998} 2036}
1999 2037
@@ -2158,7 +2196,6 @@ void GMainWindow::UpdateUITheme() {
2158 } 2196 }
2159 2197
2160 QIcon::setThemeSearchPaths(theme_paths); 2198 QIcon::setThemeSearchPaths(theme_paths);
2161 emit UpdateThemedIcons();
2162} 2199}
2163 2200
2164void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { 2201void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 501608ddc..b7398b6c7 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -30,6 +30,7 @@ class ProfilerWidget;
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(QString path);
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..76348db69 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -8,6 +8,7 @@
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>
13#include "common/common_types.h" 14#include "common/common_types.h"
@@ -25,6 +26,18 @@ struct Shortcut {
25using Themes = std::array<std::pair<const char*, const char*>, 2>; 26using Themes = std::array<std::pair<const char*, const char*>, 2>;
26extern const Themes themes; 27extern const Themes themes;
27 28
29struct GameDir {
30 QString path;
31 bool deep_scan;
32 bool expanded;
33 bool operator==(const GameDir& rhs) const {
34 return path == rhs.path;
35 };
36 bool operator!=(const GameDir& rhs) const {
37 return !operator==(rhs);
38 };
39};
40
28struct Values { 41struct Values {
29 QByteArray geometry; 42 QByteArray geometry;
30 QByteArray state; 43 QByteArray state;
@@ -55,8 +68,9 @@ struct Values {
55 QString roms_path; 68 QString roms_path;
56 QString symbols_path; 69 QString symbols_path;
57 QString screenshot_path; 70 QString screenshot_path;
58 QString game_directory_path; 71 QString game_dir_deprecated;
59 bool game_directory_deepscan; 72 bool game_dir_deprecated_deepscan;
73 QList<UISettings::GameDir> game_dirs;
60 QStringList recent_files; 74 QStringList recent_files;
61 75
62 QString theme; 76 QString theme;
@@ -84,3 +98,5 @@ struct Values {
84 98
85extern Values values; 99extern Values values;
86} // namespace UISettings 100} // namespace UISettings
101
102Q_DECLARE_METATYPE(UISettings::GameDir*);