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>

@ -6,6 +6,20 @@
<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/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource>
</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]
Name=default
Comment=default theme
Directories=16x16,256x256
Directories=16x16,48x48,256x256
[16x16]
Size=16
[48x48]
Size=48
[256x256]
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
Comment=dark theme
Inherits=default
Directories=16x16,256x256
Directories=16x16,48x48,256x256
[16x16]
Size=16
[48x48]
Size=48
[256x256]
Size=256

@ -1,6 +1,13 @@
<RCC>
<qresource prefix="icons/qdarkstyle">
<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="qss_icons">
<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
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
The icons used in this project have the following licenses:
Icon Name | License | Origin/Author
--- | --- | ---
checked.png | Free for non-commercial use
failed.png | Free for non-commercial use
lock.png | CC BY-ND 3.0 | https://icons8.com
plus_folder.png | CC BY-ND 3.0 | https://icons8.com
bad_folder.png | CC BY-ND 3.0 | https://icons8.com
chip.png | CC BY-ND 3.0 | https://icons8.com
folder.png | CC BY-ND 3.0 | https://icons8.com
plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
sd_card.png | CC BY-ND 3.0 | https://icons8.com

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

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

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

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

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

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

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

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

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

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

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

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