Merge pull request #2444 from FearlessTobi/port-3617-new

Port citra-emu/citra#3617: "QT: Add support for multiple game directories"
master
bunnei 2019-09-04 11:40:35 +07:00 committed by GitHub
commit a139fdf4ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 868 additions and 197 deletions

31
dist/license.md vendored

@ -0,0 +1,31 @@
The icons in this folder and its subfolders have the following licenses:
Icon Name | License | Origin/Author
--- | --- | ---
qt_themes/default/icons/16x16/checked.png | Free for non-commercial use
qt_themes/default/icons/16x16/failed.png | Free for non-commercial use
qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/16x16/checked.png | Free for non-commercial use
qt_themes/qdarkstyle/icons/16x16/failed.png | Free for non-commercial use
qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
<!-- TODO: Add the license of the yuzu icon -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

@ -0,0 +1,14 @@
[Icon Theme]
Name=colorful
Comment=Colorful theme
Inherits=default
Directories=16x16,48x48,256x256
[16x16]
Size=16
[48x48]
Size=48
[256x256]
Size=256

@ -0,0 +1,15 @@
<RCC>
<qresource prefix="icons/colorful">
<file alias="index.theme">icons/index.theme</file>
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource>
<qresource prefix="colorful">
<file>style.qss</file>
</qresource>
</RCC>

@ -0,0 +1,4 @@
/*
This file is intentionally left blank.
We do not want to apply any stylesheet for colorful, only icons.
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

@ -0,0 +1,8 @@
[Icon Theme]
Name=colorful_dark
Comment=Colorful theme (Dark style)
Inherits=default
Directories=16x16
[16x16]
Size=16

@ -0,0 +1,57 @@
<RCC>
<qresource prefix="icons/colorful_dark">
<file alias="index.theme">icons/index.theme</file>
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
<file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
<file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
<file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file>
<file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>
<file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file>
</qresource>
<qresource prefix="qss_icons">
<file alias="rc/up_arrow_disabled.png">../qdarkstyle/rc/up_arrow_disabled.png</file>
<file alias="rc/Hmovetoolbar.png">../qdarkstyle/rc/Hmovetoolbar.png</file>
<file alias="rc/stylesheet-branch-end.png">../qdarkstyle/rc/stylesheet-branch-end.png</file>
<file alias="rc/branch_closed-on.png">../qdarkstyle/rc/branch_closed-on.png</file>
<file alias="rc/stylesheet-vline.png">../qdarkstyle/rc/stylesheet-vline.png</file>
<file alias="rc/branch_closed.png">../qdarkstyle/rc/branch_closed.png</file>
<file alias="rc/branch_open-on.png">../qdarkstyle/rc/branch_open-on.png</file>
<file alias="rc/transparent.png">../qdarkstyle/rc/transparent.png</file>
<file alias="rc/right_arrow_disabled.png">../qdarkstyle/rc/right_arrow_disabled.png</file>
<file alias="rc/sizegrip.png">../qdarkstyle/rc/sizegrip.png</file>
<file alias="rc/close.png">../qdarkstyle/rc/close.png</file>
<file alias="rc/close-hover.png">../qdarkstyle/rc/close-hover.png</file>
<file alias="rc/close-pressed.png">../qdarkstyle/rc/close-pressed.png</file>
<file alias="rc/down_arrow.png">../qdarkstyle/rc/down_arrow.png</file>
<file alias="rc/Vmovetoolbar.png">../qdarkstyle/rc/Vmovetoolbar.png</file>
<file alias="rc/left_arrow.png">../qdarkstyle/rc/left_arrow.png</file>
<file alias="rc/stylesheet-branch-more.png">../qdarkstyle/rc/stylesheet-branch-more.png</file>
<file alias="rc/up_arrow.png">../qdarkstyle/rc/up_arrow.png</file>
<file alias="rc/right_arrow.png">../qdarkstyle/rc/right_arrow.png</file>
<file alias="rc/left_arrow_disabled.png">../qdarkstyle/rc/left_arrow_disabled.png</file>
<file alias="rc/Hsepartoolbar.png">../qdarkstyle/rc/Hsepartoolbar.png</file>
<file alias="rc/branch_open.png">../qdarkstyle/rc/branch_open.png</file>
<file alias="rc/Vsepartoolbar.png">../qdarkstyle/rc/Vsepartoolbar.png</file>
<file alias="rc/down_arrow_disabled.png">../qdarkstyle/rc/down_arrow_disabled.png</file>
<file alias="rc/undock.png">../qdarkstyle/rc/undock.png</file>
<file alias="rc/checkbox_checked_disabled.png">../qdarkstyle/rc/checkbox_checked_disabled.png</file>
<file alias="rc/checkbox_checked_focus.png">../qdarkstyle/rc/checkbox_checked_focus.png</file>
<file alias="rc/checkbox_checked.png">../qdarkstyle/rc/checkbox_checked.png</file>
<file alias="rc/checkbox_indeterminate.png">../qdarkstyle/rc/checkbox_indeterminate.png</file>
<file alias="rc/checkbox_indeterminate_focus.png">../qdarkstyle/rc/checkbox_indeterminate_focus.png</file>
<file alias="rc/checkbox_unchecked_disabled.png">../qdarkstyle/rc/checkbox_unchecked_disabled.png</file>
<file alias="rc/checkbox_unchecked_focus.png">../qdarkstyle/rc/checkbox_unchecked_focus.png</file>
<file alias="rc/checkbox_unchecked.png">../qdarkstyle/rc/checkbox_unchecked.png</file>
<file alias="rc/radio_checked_disabled.png">../qdarkstyle/rc/radio_checked_disabled.png</file>
<file alias="rc/radio_checked_focus.png">../qdarkstyle/rc/radio_checked_focus.png</file>
<file alias="rc/radio_checked.png">../qdarkstyle/rc/radio_checked.png</file>
<file alias="rc/radio_unchecked_disabled.png">../qdarkstyle/rc/radio_unchecked_disabled.png</file>
<file alias="rc/radio_unchecked_focus.png">../qdarkstyle/rc/radio_unchecked_focus.png</file>
<file alias="rc/radio_unchecked.png">../qdarkstyle/rc/radio_unchecked.png</file>
</qresource>
<qresource prefix="colorful_dark">
<file alias="style.qss">../qdarkstyle/style.qss</file>
</qresource>
</RCC>

@ -5,7 +5,21 @@
<file alias="16x16/checked.png">icons/16x16/checked.png</file> <file alias="16x16/checked.png">icons/16x16/checked.png</file>
<file alias="16x16/failed.png">icons/16x16/failed.png</file> <file alias="16x16/failed.png">icons/16x16/failed.png</file>
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
<file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file> <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file>
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

@ -1,10 +1,13 @@
[Icon Theme] [Icon Theme]
Name=default Name=default
Comment=default theme Comment=default theme
Directories=16x16,256x256 Directories=16x16,48x48,256x256
[16x16] [16x16]
Size=16 Size=16
[48x48]
Size=48
[256x256] [256x256]
Size=256 Size=256

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

@ -2,10 +2,13 @@
Name=qdarkstyle Name=qdarkstyle
Comment=dark theme Comment=dark theme
Inherits=default Inherits=default
Directories=16x16,256x256 Directories=16x16,48x48,256x256
[16x16] [16x16]
Size=16 Size=16
[48x48]
Size=48
[256x256] [256x256]
Size=256 Size=256

@ -1,6 +1,13 @@
<RCC> <RCC>
<qresource prefix="icons/qdarkstyle"> <qresource prefix="icons/qdarkstyle">
<file alias="index.theme">icons/index.theme</file> <file alias="index.theme">icons/index.theme</file>
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource> </qresource>
<qresource prefix="qss_icons"> <qresource prefix="qss_icons">
<file>rc/up_arrow_disabled.png</file> <file>rc/up_arrow_disabled.png</file>

@ -337,3 +337,19 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. Public License instead of this License.
The icons used in this project have the following licenses:
Icon Name | License | Origin/Author
--- | --- | ---
checked.png | Free for non-commercial use
failed.png | Free for non-commercial use
lock.png | CC BY-ND 3.0 | https://icons8.com
plus_folder.png | CC BY-ND 3.0 | https://icons8.com
bad_folder.png | CC BY-ND 3.0 | https://icons8.com
chip.png | CC BY-ND 3.0 | https://icons8.com
folder.png | CC BY-ND 3.0 | https://icons8.com
plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
sd_card.png | CC BY-ND 3.0 | https://icons8.com

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

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

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

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

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

@ -10,6 +10,7 @@
#include <utility> #include <utility>
#include <QCoreApplication> #include <QCoreApplication>
#include <QFileInfo>
#include <QImage> #include <QImage>
#include <QObject> #include <QObject>
#include <QStandardItem> #include <QStandardItem>
@ -22,6 +23,17 @@
#include "yuzu/uisettings.h" #include "yuzu/uisettings.h"
#include "yuzu/util/util.h" #include "yuzu/util/util.h"
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
CustomDir = QStandardItem::UserType + 2,
SdmcDir = QStandardItem::UserType + 3,
UserNandDir = QStandardItem::UserType + 4,
SysNandDir = QStandardItem::UserType + 5,
AddDir = QStandardItem::UserType + 6
};
Q_DECLARE_METATYPE(GameListItemType);
/** /**
* Gets the default icon (for games without valid title metadata) * Gets the default icon (for games without valid title metadata)
* @param size The desired width and height of the default icon. * @param size The desired width and height of the default icon.
@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) {
class GameListItem : public QStandardItem { class GameListItem : public QStandardItem {
public: public:
// used to access type from item index
static const int TypeRole = Qt::UserRole + 1;
static const int SortRole = Qt::UserRole + 2;
GameListItem() = default; GameListItem() = default;
explicit GameListItem(const QString& string) : QStandardItem(string) {} GameListItem(const QString& string) : QStandardItem(string) {
setData(string, SortRole);
}
}; };
/** /**
@ -48,14 +65,15 @@ public:
*/ */
class GameListItemPath : public GameListItem { class GameListItemPath : public GameListItem {
public: public:
static const int FullPathRole = Qt::UserRole + 1; static const int TitleRole = SortRole;
static const int TitleRole = Qt::UserRole + 2; static const int FullPathRole = SortRole + 1;
static const int ProgramIdRole = Qt::UserRole + 3; static const int ProgramIdRole = SortRole + 2;
static const int FileTypeRole = Qt::UserRole + 4; static const int FileTypeRole = SortRole + 3;
GameListItemPath() = default; GameListItemPath() = default;
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
const QString& game_name, const QString& game_type, u64 program_id) { const QString& game_name, const QString& game_type, u64 program_id) {
setData(type(), TypeRole);
setData(game_path, FullPathRole); setData(game_path, FullPathRole);
setData(game_name, TitleRole); setData(game_name, TitleRole);
setData(qulonglong(program_id), ProgramIdRole); setData(qulonglong(program_id), ProgramIdRole);
@ -72,6 +90,10 @@ public:
setData(picture, Qt::DecorationRole); setData(picture, Qt::DecorationRole);
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
QVariant data(int role) const override { QVariant data(int role) const override {
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
std::string filename; std::string filename;
@ -103,9 +125,11 @@ public:
class GameListItemCompat : public GameListItem { class GameListItemCompat : public GameListItem {
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
public: public:
static const int CompatNumberRole = Qt::UserRole + 1; static const int CompatNumberRole = SortRole;
GameListItemCompat() = default; GameListItemCompat() = default;
explicit GameListItemCompat(const QString& compatibility) { explicit GameListItemCompat(const QString& compatibility) {
setData(type(), TypeRole);
struct CompatStatus { struct CompatStatus {
QString color; QString color;
const char* text; const char* text;
@ -135,6 +159,10 @@ public:
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
bool operator<(const QStandardItem& other) const override { bool operator<(const QStandardItem& other) const override {
return data(CompatNumberRole) < other.data(CompatNumberRole); return data(CompatNumberRole) < other.data(CompatNumberRole);
} }
@ -146,12 +174,12 @@ public:
* human-readable string representation will be displayed to the user. * human-readable string representation will be displayed to the user.
*/ */
class GameListItemSize : public GameListItem { class GameListItemSize : public GameListItem {
public: public:
static const int SizeRole = Qt::UserRole + 1; static const int SizeRole = SortRole;
GameListItemSize() = default; GameListItemSize() = default;
explicit GameListItemSize(const qulonglong size_bytes) { explicit GameListItemSize(const qulonglong size_bytes) {
setData(type(), TypeRole);
setData(size_bytes, SizeRole); setData(size_bytes, SizeRole);
} }
@ -167,6 +195,10 @@ public:
} }
} }
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
/** /**
* This operator is, in practice, only used by the TreeView sorting systems. * This operator is, in practice, only used by the TreeView sorting systems.
* Override it so that it will correctly sort by numerical value instead of by string * Override it so that it will correctly sort by numerical value instead of by string
@ -177,6 +209,82 @@ public:
} }
}; };
class GameListDir : public GameListItem {
public:
static const int GameDirRole = Qt::UserRole + 2;
explicit GameListDir(UISettings::GameDir& directory,
GameListItemType dir_type = GameListItemType::CustomDir)
: dir_type{dir_type} {
setData(type(), TypeRole);
UISettings::GameDir* game_dir = &directory;
setData(QVariant::fromValue(game_dir), GameDirRole);
const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
switch (dir_type) {
case GameListItemType::SdmcDir:
setData(
QIcon::fromTheme(QStringLiteral("sd_card"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
break;
case GameListItemType::UserNandDir:
setData(
QIcon::fromTheme(QStringLiteral("chip"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
break;
case GameListItemType::SysNandDir:
setData(
QIcon::fromTheme(QStringLiteral("chip"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(QObject::tr("System Titles"), Qt::DisplayRole);
break;
case GameListItemType::CustomDir:
const QString icon_name = QFileInfo::exists(game_dir->path)
? QStringLiteral("folder")
: QStringLiteral("bad_folder");
setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(game_dir->path, Qt::DisplayRole);
break;
};
};
int type() const override {
return static_cast<int>(dir_type);
}
private:
GameListItemType dir_type;
};
class GameListAddDir : public GameListItem {
public:
explicit GameListAddDir() {
setData(type(), TypeRole);
const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
setData(QIcon::fromTheme(QStringLiteral("plus"))
.pixmap(icon_size)
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
Qt::DecorationRole);
setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
}
int type() const override {
return static_cast<int>(GameListItemType::AddDir);
}
};
class GameList; class GameList;
class QHBoxLayout; class QHBoxLayout;
class QTreeView; class QTreeView;
@ -208,6 +316,9 @@ private:
// EventFilter in order to process systemkeys while editing the searchfield // EventFilter in order to process systemkeys while editing the searchfield
bool eventFilter(QObject* obj, QEvent* event) override; bool eventFilter(QObject* obj, QEvent* event) override;
}; };
int visible;
int total;
QHBoxLayout* layout_filter = nullptr; QHBoxLayout* layout_filter = nullptr;
QTreeView* tree_view = nullptr; QTreeView* tree_view = nullptr;
QLabel* label_filter = nullptr; QLabel* label_filter = nullptr;

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

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

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

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

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

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