citra_qt: support multiple game directories

master
BreadFish64 2018-04-19 19:56:24 +07:00
parent 88ebd844e5
commit 1a57f9488f
26 changed files with 561 additions and 132 deletions

@ -12,6 +12,18 @@
<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/citra.png">icons/256x256/citra.png</file>
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 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: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 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

@ -2,6 +2,12 @@
<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>

@ -202,8 +202,34 @@ void Config::ReadValues() {
qt_config->beginGroup("Paths");
UISettings::values.roms_path = qt_config->value("romsPath").toString();
UISettings::values.symbols_path = qt_config->value("symbolsPath").toString();
UISettings::values.gamedir = qt_config->value("gameListRootDir", ".").toString();
UISettings::values.gamedir_deepscan = qt_config->value("gameListDeepScan", false).toBool();
UISettings::values.game_dir_deprecated = qt_config->value("gameListRootDir", ".").toString();
UISettings::values.game_dir_deprecated_deepscan =
qt_config->value("gameListDeepScan", false).toBool();
int size = qt_config->beginReadArray("gamedirs");
for (int i = 0; i < size; ++i) {
qt_config->setArrayIndex(i);
UISettings::GameDir game_dir;
game_dir.path = qt_config->value("path").toString();
game_dir.deep_scan = qt_config->value("deep_scan", false).toBool();
game_dir.expanded = qt_config->value("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 = "INSTALLED";
game_dir.expanded = true;
UISettings::values.game_dirs.append(game_dir);
game_dir.path = "SYSTEM";
UISettings::values.game_dirs.append(game_dir);
if (UISettings::values.game_dir_deprecated != ".") {
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 = qt_config->value("recentFiles").toStringList();
UISettings::values.language = qt_config->value("language", "").toString();
qt_config->endGroup();
@ -378,8 +404,15 @@ void Config::SaveValues() {
qt_config->beginGroup("Paths");
qt_config->setValue("romsPath", UISettings::values.roms_path);
qt_config->setValue("symbolsPath", UISettings::values.symbols_path);
qt_config->setValue("gameListRootDir", UISettings::values.gamedir);
qt_config->setValue("gameListDeepScan", UISettings::values.gamedir_deepscan);
qt_config->beginWriteArray("gamedirs");
for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
qt_config->setArrayIndex(i);
const auto& game_dir = UISettings::values.game_dirs.at(i);
qt_config->setValue("path", game_dir.path);
qt_config->setValue("deep_scan", game_dir.deep_scan);
qt_config->setValue("expanded", game_dir.expanded);
}
qt_config->endArray();
qt_config->setValue("recentFiles", UISettings::values.recent_files);
qt_config->setValue("language", UISettings::values.language);
qt_config->endGroup();

@ -44,7 +44,6 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
ConfigureGeneral::~ConfigureGeneral() {}
void ConfigureGeneral::setConfiguration() {
ui->toggle_deepscan->setChecked(UISettings::values.gamedir_deepscan);
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit);
@ -60,7 +59,6 @@ void ConfigureGeneral::setConfiguration() {
}
void ConfigureGeneral::applyConfiguration() {
UISettings::values.gamedir_deepscan = ui->toggle_deepscan->isChecked();
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
UISettings::values.theme =
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>345</width>
<height>493</height>
<height>504</height>
</rect>
</property>
<property name="windowTitle">
@ -31,13 +31,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_deepscan">
<property name="text">
<string>Search sub-directories for games</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>

@ -66,6 +66,7 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
case Qt::Key_Enter: {
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 diffrent game after closing one
gamelist->search_field->edit_filter->setText("");
@ -171,6 +172,15 @@ bool GameList::containsAllWords(QString haystack, QString userinput) {
[haystack](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) {
GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>();
if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir ||
type == GameListItemType::SystemDir)
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& newText) {
int folderCount = tree_view->model()->rowCount();
@ -226,6 +236,31 @@ void GameList::onTextChanged(const QString& newText) {
}
}
void GameList::onUpdateThemedIcons() {
for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
QStandardItem* child = item_model->invisibleRootItem()->child(i);
switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::InstalledDir:
child->setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole);
break;
case GameListItemType::SystemDir:
child->setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole);
break;
case GameListItemType::CustomDir: {
const UISettings::GameDir* game_dir =
child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
child->setData(QIcon::fromTheme(icon_name).pixmap(48), Qt::DecorationRole);
break;
}
case GameListItemType::AddDir:
child->setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole);
break;
}
}
}
void GameList::onFilterCloseClicked() {
main_window->filterBarSetChecked(false);
}
@ -257,12 +292,16 @@ GameList::GameList(GMainWindow* parent) : QWidget{parent} {
item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, "Region");
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "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);
@ -290,27 +329,57 @@ 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.
int row = item_model->itemFromIndex(item)->row();
QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
QString file_path = child_file->data(GameListItemPath::FullPathRole).toString();
auto selected = item.sibling(item.row(), 0);
if (file_path.isEmpty())
return;
std::string std_file_path(file_path.toStdString());
if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
return;
// Users usually want to run a diffrent game after closing one
search_field->clear();
emit GameChosen(file_path);
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::Game: {
QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
if (file_path.isEmpty())
return;
QFileInfo file_info(file_path);
if (!file_info.exists() || file_info.isDir())
return;
// 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() {
for (int i = 0; i < item_model->rowCount(); i++) {
const QStandardItem* child = item_model->invisibleRootItem()->child(i);
GameListItemType type = static_cast<GameListItemType>(child->type());
if (!child->hasChildren() &&
(type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) {
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()) {
@ -346,12 +415,25 @@ 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();
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());
break;
case GameListItemType::CustomDir:
AddPermDirPopup(context_menu, selected);
AddCustomDirPopup(context_menu, selected);
break;
case GameListItemType::InstalledDir:
case GameListItemType::SystemDir:
AddPermDirPopup(context_menu, selected);
break;
}
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
}
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id) {
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* open_application_location = context_menu.addAction(tr("Open Application Location"));
QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location"));
@ -370,16 +452,81 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
});
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
connect(open_save_location, &QAction::triggered,
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); });
connect(open_application_location, &QAction::triggered,
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); });
connect(open_update_location, &QAction::triggered,
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); });
connect(navigate_to_gamedb_entry, &QAction::triggered,
[&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); });
connect(open_save_location, &QAction::triggered, [this, program_id] {
emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA);
});
connect(open_application_location, &QAction::triggered, [this, program_id] {
emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION);
});
connect(open_update_location, &QAction::triggered, [this, program_id] {
emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA);
});
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
});
};
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"));
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
UISettings::values.game_dirs.swap(
UISettings::values.game_dirs.indexOf(game_dir),
UISettings::values.game_dirs.indexOf(*selected.sibling(selected.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
UISettings::values.game_dirs.swap(
UISettings::values.game_dirs.indexOf(game_dir),
UISettings::values.game_dirs.indexOf(*selected.sibling(selected.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(open_directory_location, &QAction::triggered,
[this, game_dir] { emit OpenDirectory(game_dir.path); });
}
void GameList::LoadCompatibilityList() {
@ -428,14 +575,7 @@ QStandardItemModel* GameList::GetModel() const {
return item_model;
}
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
if (!FileUtil::Exists(dir_path.toStdString()) ||
!FileUtil::IsDirectory(dir_path.toStdString())) {
NGLOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString());
search_field->setFilterResult(0, 0);
return;
}
void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) {
tree_view->setEnabled(false);
// Delete any rows that might already exist if we're repopulating
item_model->removeRows(0, item_model->rowCount());
@ -443,13 +583,15 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
emit ShouldCancelWorker();
GameListWorker* worker = new GameListWorker(dir_path, deep_scan, compatibility_list);
GameListWorker* worker = new GameListWorker(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);
@ -481,15 +623,17 @@ static bool HasSupportedFileExtension(const std::string& file_name) {
}
void GameList::RefreshGameDirectory() {
if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
NGLOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
PopulateAsync(UISettings::values.gamedirs);
PopulateAsync(UISettings::values.game_dirs);
}
}
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool {
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
GameListDir* parent_dir) {
const auto callback = [this, recursion, parent_dir](unsigned* num_entries_out,
const std::string& directory,
const std::string& virtual_name) -> bool {
std::string physical_name = directory + DIR_SEP + virtual_name;
if (stop_processing)
@ -539,17 +683,20 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
if (it != compatibility_list.end())
compatibility = it->second.first;
emit EntryReady({
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
new GameListItemCompat(compatibility),
new GameListItemRegion(smdh),
new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)),
});
emit EntryReady(
{
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
new GameListItemCompat(compatibility),
new GameListItemRegion(smdh),
new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)),
},
parent_dir);
} else if (is_dir && recursion > 0) {
watch_list.append(QString::fromStdString(physical_name));
AddFstEntriesToGameList(physical_name, recursion - 1);
AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir);
}
return true;
@ -560,27 +707,33 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
void GameListWorker::run() {
stop_processing = false;
watch_list.append(dir_path);
watch_list.append(QString::fromStdString(
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
"Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000"));
watch_list.append(QString::fromStdString(
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
"Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004000e"));
watch_list.append(
QString::fromStdString(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
"00000000000000000000000000000000/title/00040010"));
AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
AddFstEntriesToGameList(
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
"Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000",
2);
AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
"00000000000000000000000000000000/title/00040010",
2);
for (UISettings::GameDir& game_dir : game_dirs) {
if (game_dir.path == "INSTALLED") {
QString path = QString(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
"Nintendo "
"3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000";
watch_list.append(path);
GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
emit DirEntryReady({game_list_dir});
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir);
} else if (game_dir.path == "SYSTEM") {
QString path = QString(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
"00000000000000000000000000000000/title/00040010";
watch_list.append(path);
GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
emit DirEntryReady({game_list_dir});
AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
"00000000000000000000000000000000/title/00040010",
2, game_list_dir);
} else {
watch_list.append(game_dir.path);
GameListDir* game_list_dir = new GameListDir(game_dir);
emit DirEntryReady({game_list_dir});
AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0,
game_list_dir);
}
};
emit Finished(watch_list);
}
@ -588,3 +741,37 @@ void GameListWorker::Cancel() {
this->disconnect();
stop_processing = true;
}
GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
this->main_window = parent;
connect(main_window, &GMainWindow::UpdateThemedIcons, this,
&GameListPlaceholder::onUpdateThemedIcons);
layout = new QVBoxLayout;
image = new QLabel;
text = new QLabel;
layout->setAlignment(Qt::AlignCenter);
image->setPixmap(QIcon::fromTheme("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("plus_folder").pixmap(200));
}
void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
emit GameListPlaceholder::AddDirectory();
}

@ -8,13 +8,17 @@
#include <QString>
#include <QWidget>
#include "common/common_types.h"
#include "ui_settings.h"
class GameListWorker;
class GameListDir;
class GMainWindow;
class QFileSystemWatcher;
class QHBoxLayout;
class QLabel;
class QLineEdit;
template <typename>
class QList;
class QModelIndex;
class QStandardItem;
class QStandardItemModel;
@ -39,12 +43,14 @@ public:
class SearchField : public QWidget {
public:
int visible;
int total;
explicit SearchField(GameList* parent = nullptr);
void setFilterResult(int visible, int total);
void clear();
void setFocus();
explicit SearchField(GameList* parent = nullptr);
int visible;
int total;
private:
class KeyReleaseEater : public QObject {
@ -73,9 +79,10 @@ public:
void clearFilter();
void setFilterFocus();
void setFilterVisible(bool visibility);
bool isEmpty();
void LoadCompatibilityList();
void PopulateAsync(const QString& dir_path, bool deep_scan);
void PopulateAsync(QList<UISettings::GameDir>& game_dirs);
void SaveInterfaceLayout();
void LoadInterfaceLayout();
@ -91,20 +98,30 @@ signals:
void NavigateToGamedbEntryRequested(
u64 program_id,
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
void OpenDirectory(QString directory);
void AddDirectory();
void ShowList(bool show);
private slots:
void onItemExpanded(const QModelIndex& item);
void onTextChanged(const QString& newText);
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();
bool containsAllWords(QString haystack, QString userinput);
void PopupContextMenu(const QPoint& menu_location);
void AddGamePopup(QMenu& context_menu, u64 program_id);
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
SearchField* search_field;
GMainWindow* main_window = nullptr;
QVBoxLayout* layout = nullptr;
@ -116,3 +133,25 @@ 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:
GMainWindow* main_window = nullptr;
QVBoxLayout* layout = nullptr;
QLabel* image = nullptr;
QLabel* text = nullptr;
};

@ -8,17 +8,29 @@
#include <map>
#include <unordered_map>
#include <QCoreApplication>
#include <QFileInfo>
#include <QImage>
#include <QObject>
#include <QPainter>
#include <QRunnable>
#include <QStandardItem>
#include <QString>
#include "citra_qt/ui_settings.h"
#include "citra_qt/util/util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/loader/smdh.h"
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
CustomDir = QStandardItem::UserType + 2,
InstalledDir = QStandardItem::UserType + 3,
SystemDir = QStandardItem::UserType + 4,
AddDir = QStandardItem::UserType + 5
};
Q_DECLARE_METATYPE(GameListItemType);
/**
* Gets the game icon from SMDH data.
* @param smdh SMDH data
@ -125,8 +137,13 @@ const static inline std::map<QString, CompatStatus> status_data = {
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() : QStandardItem() {}
GameListItem(const QString& string) : QStandardItem(string) {}
GameListItem(const QString& string) : QStandardItem(string) {
setData(string, SortRole);
}
virtual ~GameListItem() override {}
};
@ -138,13 +155,14 @@ 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 TitleRole = SortRole;
static const int FullPathRole = SortRole + 1;
static const int ProgramIdRole = SortRole + 2;
GameListItemPath() : GameListItem() {}
GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id)
: GameListItem() {
setData(type(), TypeRole);
setData(game_path, FullPathRole);
setData(qulonglong(program_id), ProgramIdRole);
@ -165,6 +183,10 @@ public:
TitleRole);
}
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
QVariant data(int role) const override {
if (role == Qt::DisplayRole) {
std::string filename;
@ -180,9 +202,12 @@ public:
class GameListItemCompat : public GameListItem {
public:
static const int CompatNumberRole = Qt::UserRole + 1;
static const int CompatNumberRole = SortRole;
GameListItemCompat() = default;
explicit GameListItemCompat(const QString compatiblity) {
setData(type(), TypeRole);
auto iterator = status_data.find(compatiblity);
if (iterator == status_data.end()) {
NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString());
@ -195,6 +220,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);
}
@ -204,6 +233,8 @@ class GameListItemRegion : public GameListItem {
public:
GameListItemRegion() = default;
explicit GameListItemRegion(const std::vector<u8>& smdh_data) {
setData(type(), TypeRole);
if (!Loader::IsValidSMDH(smdh_data)) {
setText(QObject::tr("Invalid region"));
return;
@ -213,6 +244,11 @@ public:
memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
setText(GetRegionFromSMDH(smdh));
setData(GetRegionFromSMDH(smdh), SortRole);
}
int type() const override {
return static_cast<int>(GameListItemType::Game);
}
};
@ -223,10 +259,11 @@ public:
*/
class GameListItemSize : public GameListItem {
public:
static const int SizeRole = Qt::UserRole + 1;
static const int SizeRole = SortRole;
GameListItemSize() : GameListItem() {}
GameListItemSize(const qulonglong size_bytes) : GameListItem() {
setData(type(), TypeRole);
setData(size_bytes, SizeRole);
}
@ -242,6 +279,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
@ -252,6 +293,55 @@ 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);
switch (dir_type) {
case GameListItemType::InstalledDir:
setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole);
setData("Installed Titles", Qt::DisplayRole);
break;
case GameListItemType::SystemDir:
setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole);
setData("System Titles", Qt::DisplayRole);
break;
case GameListItemType::CustomDir:
QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
setData(QIcon::fromTheme(icon_name).pixmap(48), 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);
setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole);
setData("Add New Game Directory", Qt::DisplayRole);
}
int type() const override {
return static_cast<int>(GameListItemType::AddDir);
}
};
/**
* Asynchronous worker object for populating the game list.
* Communicates with other threads through Qt's signal/slot system.
@ -260,11 +350,10 @@ class GameListWorker : public QObject, public QRunnable {
Q_OBJECT
public:
GameListWorker(
QString dir_path, bool deep_scan,
explicit GameListWorker(
QList<UISettings::GameDir>& game_dirs,
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list)
: QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan),
compatibility_list(compatibility_list) {}
: QObject(), QRunnable(), game_dirs(game_dirs), compatibility_list(compatibility_list) {}
public slots:
/// Starts the processing of directory tree information.
@ -276,22 +365,24 @@ 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 emmited
* 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:
QStringList watch_list;
QString dir_path;
bool deep_scan;
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
QList<UISettings::GameDir>& game_dirs;
std::atomic_bool stop_processing;
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
GameListDir* parent_dir);
};

@ -143,7 +143,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
show();
game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
game_list->PopulateAsync(UISettings::values.game_dirs);
// Show one-time "callout" messages to the user
ShowCallouts();
@ -177,6 +177,10 @@ void GMainWindow::InitializeWidgets() {
game_list = new GameList(this);
ui.horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
ui.horizontalLayout->addWidget(game_list_placeholder);
game_list_placeholder->setVisible(false);
multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
ui.action_Show_Room);
multiplayer_state->setVisible(false);
@ -399,9 +403,14 @@ 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::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(this, &GMainWindow::EmulationStarting, render_window,
&GRenderWindow::OnEmulationStarting);
@ -419,8 +428,6 @@ void GMainWindow::ConnectMenuEvents() {
// File
connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
connect(ui.action_Install_CIA, &QAction::triggered, this, &GMainWindow::OnMenuInstallCIA);
connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
&GMainWindow::OnMenuSelectGameListRoot);
connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
// Emulation
@ -672,6 +679,7 @@ void GMainWindow::BootGame(const QString& filename) {
registersWidget->OnDebugModeEntered();
if (ui.action_Single_Window_Mode->isChecked()) {
game_list->hide();
game_list_placeholder->hide();
}
status_bar_update_timer.start(2000);
@ -713,7 +721,10 @@ void GMainWindow::ShutdownGame() {
ui.action_Stop->setEnabled(false);
ui.action_Report_Compatibility->setEnabled(false);
render_window->hide();
game_list->show();
if (game_list->isEmpty())
game_list_placeholder->show();
else
game_list->show();
game_list->setFilterFocus();
// Disable status bar updates
@ -828,6 +839,48 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(
QDesktopServices::openUrl(QUrl("https://citra-emu.org/game/" + directory));
}
void GMainWindow::OnGameListOpenDirectory(QString directory) {
QString path;
if (directory == "INSTALLED") {
path =
QString::fromStdString(FileUtil::GetUserPath(D_SDMC_IDX).c_str() +
std::string("Nintendo "
"3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000"));
} else if (directory == "SYSTEM") {
path =
QString::fromStdString(FileUtil::GetUserPath(D_NAND_IDX).c_str() +
std::string("00000000000000000000000000000000/title/00040010"));
} 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() {
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 {
NGLOG_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::OnMenuLoadFile() {
QString extensions;
for (const auto& piece : game_list->supported_file_extensions)
@ -845,14 +898,6 @@ void GMainWindow::OnMenuLoadFile() {
}
}
void GMainWindow::OnMenuSelectGameListRoot() {
QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
if (!dir_path.isEmpty()) {
UISettings::values.gamedir = dir_path;
game_list->PopulateAsync(dir_path, UISettings::values.gamedir_deepscan);
}
}
void GMainWindow::OnMenuInstallCIA() {
QStringList filepaths = QFileDialog::getOpenFileNames(
this, tr("Load Files"), UISettings::values.roms_path,
@ -1089,6 +1134,7 @@ void GMainWindow::OnConfigure() {
if (result == QDialog::Accepted) {
configureDialog.applyConfiguration();
UpdateUITheme();
emit UpdateThemedIcons();
SyncMenuUISettings();
config->Save();
}
@ -1308,7 +1354,6 @@ void GMainWindow::UpdateUITheme() {
QIcon::setThemeName(":/icons/default");
}
QIcon::setThemeSearchPaths(theme_paths);
emit UpdateThemedIcons();
}
void GMainWindow::LoadTranslation() {

@ -20,6 +20,7 @@ class ClickableLabel;
class EmuThread;
class GameList;
enum class GameListOpenTarget;
class GameListPlaceholder;
class GImageInfo;
class GPUCommandListWidget;
class GPUCommandStreamWidget;
@ -148,13 +149,14 @@ private slots:
void OnGameListNavigateToGamedbEntry(
u64 program_id,
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
void OnGameListOpenDirectory(QString path);
void OnGameListAddDirectory();
void OnGameListShowList(bool show);
void OnMenuLoadFile();
void OnMenuInstallCIA();
void OnUpdateProgress(size_t written, size_t total);
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
void OnCIAInstallFinished();
/// Called whenever a user selects the "File->Select Game List Root" menu item
void OnMenuSelectGameListRoot();
void OnMenuRecentFile();
void OnConfigure();
void OnToggleFilterBar();
@ -184,6 +186,8 @@ private:
GRenderWindow* render_window;
GameListPlaceholder* game_list_placeholder;
// Status bar elements
QProgressBar* progress_bar = nullptr;
QLabel* message_label = nullptr;

@ -60,7 +60,6 @@
<addaction name="action_Load_File"/>
<addaction name="action_Install_CIA"/>
<addaction name="separator"/>
<addaction name="action_Select_Game_List_Root"/>
<addaction name="menu_recent_files"/>
<addaction name="separator"/>
<addaction name="action_Exit"/>

@ -7,6 +7,7 @@
#include <array>
#include <vector>
#include <QByteArray>
#include <QMetaType>
#include <QString>
#include <QStringList>
@ -19,6 +20,18 @@ static const std::array<std::pair<QString, QString>, 2> themes = {
{std::make_pair(QString("Default"), QString("default")),
std::make_pair(QString("Dark"), QString("qdarkstyle"))}};
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;
@ -45,8 +58,9 @@ struct Values {
QString roms_path;
QString symbols_path;
QString gamedir;
bool gamedir_deepscan;
QString game_dir_deprecated;
bool game_dir_deprecated_deepscan;
QList<UISettings::GameDir> game_dirs;
QStringList recent_files;
QString language;
@ -74,3 +88,5 @@ struct Values {
extern Values values;
} // namespace UISettings
Q_DECLARE_METATYPE(UISettings::GameDir*);