Merge pull request #3617 from BreadFish64/multiple-game-dirs
QT: Add support for multiple game directories
12
dist/qt_themes/default/default.qrc
vendored
|
@ -12,6 +12,18 @@
|
||||||
|
|
||||||
<file alias="16x16/lock.png">icons/16x16/lock.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/citra.png">icons/256x256/citra.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>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
BIN
dist/qt_themes/default/icons/256x256/plus_folder.png
vendored
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
dist/qt_themes/default/icons/48x48/bad_folder.png
vendored
Normal file
After Width: | Height: | Size: 601 B |
BIN
dist/qt_themes/default/icons/48x48/chip.png
vendored
Normal file
After Width: | Height: | Size: 456 B |
BIN
dist/qt_themes/default/icons/48x48/folder.png
vendored
Normal file
After Width: | Height: | Size: 294 B |
BIN
dist/qt_themes/default/icons/48x48/plus.png
vendored
Normal file
After Width: | Height: | Size: 316 B |
BIN
dist/qt_themes/default/icons/48x48/sd_card.png
vendored
Normal file
After Width: | Height: | Size: 311 B |
5
dist/qt_themes/default/icons/index.theme
vendored
|
@ -1,10 +1,13 @@
|
||||||
[Icon Theme]
|
[Icon Theme]
|
||||||
Name=default
|
Name=default
|
||||||
Comment=default theme
|
Comment=default theme
|
||||||
Directories=16x16,256x256
|
Directories=16x16,48x48,256x256
|
||||||
|
|
||||||
[16x16]
|
[16x16]
|
||||||
Size=16
|
Size=16
|
||||||
|
|
||||||
|
[48x48]
|
||||||
|
Size=48
|
||||||
|
|
||||||
[256x256]
|
[256x256]
|
||||||
Size=256
|
Size=256
|
BIN
dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png
vendored
Normal file
After Width: | Height: | Size: 651 B |
BIN
dist/qt_themes/qdarkstyle/icons/48x48/chip.png
vendored
Normal file
After Width: | Height: | Size: 494 B |
BIN
dist/qt_themes/qdarkstyle/icons/48x48/folder.png
vendored
Normal file
After Width: | Height: | Size: 340 B |
BIN
dist/qt_themes/qdarkstyle/icons/48x48/plus.png
vendored
Normal file
After Width: | Height: | Size: 339 B |
BIN
dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png
vendored
Normal file
After Width: | Height: | Size: 327 B |
7
dist/qt_themes/qdarkstyle/icons/index.theme
vendored
|
@ -2,10 +2,13 @@
|
||||||
Name=qdarkstyle
|
Name=qdarkstyle
|
||||||
Comment=dark theme
|
Comment=dark theme
|
||||||
Inherits=default
|
Inherits=default
|
||||||
Directories=16x16,256x256
|
Directories=16x16,48x48,256x256
|
||||||
|
|
||||||
[16x16]
|
[16x16]
|
||||||
Size=16
|
Size=16
|
||||||
|
|
||||||
|
[48x48]
|
||||||
|
Size=48
|
||||||
|
|
||||||
[256x256]
|
[256x256]
|
||||||
Size=256
|
Size=256
|
6
dist/qt_themes/qdarkstyle/style.qrc
vendored
|
@ -2,6 +2,12 @@
|
||||||
<qresource prefix="icons/qdarkstyle">
|
<qresource prefix="icons/qdarkstyle">
|
||||||
<file alias="index.theme">icons/index.theme</file>
|
<file alias="index.theme">icons/index.theme</file>
|
||||||
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||||
|
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
|
||||||
|
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
|
||||||
|
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
|
||||||
|
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
|
||||||
|
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
|
||||||
|
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="qss_icons">
|
<qresource prefix="qss_icons">
|
||||||
<file>rc/up_arrow_disabled.png</file>
|
<file>rc/up_arrow_disabled.png</file>
|
||||||
|
|
|
@ -207,8 +207,34 @@ void Config::ReadValues() {
|
||||||
qt_config->beginGroup("Paths");
|
qt_config->beginGroup("Paths");
|
||||||
UISettings::values.roms_path = qt_config->value("romsPath").toString();
|
UISettings::values.roms_path = qt_config->value("romsPath").toString();
|
||||||
UISettings::values.symbols_path = qt_config->value("symbolsPath").toString();
|
UISettings::values.symbols_path = qt_config->value("symbolsPath").toString();
|
||||||
UISettings::values.gamedir = qt_config->value("gameListRootDir", ".").toString();
|
UISettings::values.game_dir_deprecated = qt_config->value("gameListRootDir", ".").toString();
|
||||||
UISettings::values.gamedir_deepscan = qt_config->value("gameListDeepScan", false).toBool();
|
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.recent_files = qt_config->value("recentFiles").toStringList();
|
||||||
UISettings::values.language = qt_config->value("language", "").toString();
|
UISettings::values.language = qt_config->value("language", "").toString();
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
|
@ -386,8 +412,15 @@ void Config::SaveValues() {
|
||||||
qt_config->beginGroup("Paths");
|
qt_config->beginGroup("Paths");
|
||||||
qt_config->setValue("romsPath", UISettings::values.roms_path);
|
qt_config->setValue("romsPath", UISettings::values.roms_path);
|
||||||
qt_config->setValue("symbolsPath", UISettings::values.symbols_path);
|
qt_config->setValue("symbolsPath", UISettings::values.symbols_path);
|
||||||
qt_config->setValue("gameListRootDir", UISettings::values.gamedir);
|
qt_config->beginWriteArray("gamedirs");
|
||||||
qt_config->setValue("gameListDeepScan", UISettings::values.gamedir_deepscan);
|
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("recentFiles", UISettings::values.recent_files);
|
||||||
qt_config->setValue("language", UISettings::values.language);
|
qt_config->setValue("language", UISettings::values.language);
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
|
|
|
@ -44,7 +44,6 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
|
||||||
ConfigureGeneral::~ConfigureGeneral() {}
|
ConfigureGeneral::~ConfigureGeneral() {}
|
||||||
|
|
||||||
void ConfigureGeneral::setConfiguration() {
|
void ConfigureGeneral::setConfiguration() {
|
||||||
ui->toggle_deepscan->setChecked(UISettings::values.gamedir_deepscan);
|
|
||||||
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
|
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
|
||||||
ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit);
|
ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit);
|
||||||
|
|
||||||
|
@ -60,7 +59,6 @@ void ConfigureGeneral::setConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureGeneral::applyConfiguration() {
|
void ConfigureGeneral::applyConfiguration() {
|
||||||
UISettings::values.gamedir_deepscan = ui->toggle_deepscan->isChecked();
|
|
||||||
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
|
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
|
||||||
UISettings::values.theme =
|
UISettings::values.theme =
|
||||||
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
|
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>345</width>
|
<width>345</width>
|
||||||
<height>493</height>
|
<height>504</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -31,13 +31,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="toggle_deepscan">
|
|
||||||
<property name="text">
|
|
||||||
<string>Search sub-directories for games</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
|
|
|
@ -43,7 +43,6 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
|
||||||
return QObject::eventFilter(obj, event);
|
return QObject::eventFilter(obj, event);
|
||||||
|
|
||||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
||||||
int rowCount = gamelist->tree_view->model()->rowCount();
|
|
||||||
QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
|
QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
|
||||||
|
|
||||||
// If the searchfield's text hasn't changed special function keys get checked
|
// If the searchfield's text hasn't changed special function keys get checked
|
||||||
|
@ -65,19 +64,9 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
|
||||||
// If there is only one result launch this game
|
// If there is only one result launch this game
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
case Qt::Key_Enter: {
|
case Qt::Key_Enter: {
|
||||||
QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view);
|
if (gamelist->search_field->visible == 1) {
|
||||||
QModelIndex root_index = item_model->invisibleRootItem()->index();
|
QString file_path = gamelist->getLastFilterResultItem();
|
||||||
QStandardItem* child_file;
|
|
||||||
QString file_path;
|
|
||||||
int resultCount = 0;
|
|
||||||
for (int i = 0; i < rowCount; ++i) {
|
|
||||||
if (!gamelist->tree_view->isRowHidden(i, root_index)) {
|
|
||||||
++resultCount;
|
|
||||||
child_file = gamelist->item_model->item(i, 0);
|
|
||||||
file_path = child_file->data(GameListItemPath::FullPathRole).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resultCount == 1) {
|
|
||||||
// To avoid loading error dialog loops while confirming them using enter
|
// To avoid loading error dialog loops while confirming them using enter
|
||||||
// Also users usually want to run a diffrent game after closing one
|
// Also users usually want to run a diffrent game after closing one
|
||||||
gamelist->search_field->edit_filter->setText("");
|
gamelist->search_field->edit_filter->setText("");
|
||||||
|
@ -97,6 +86,9 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::SearchField::setFilterResult(int visible, int total) {
|
void GameList::SearchField::setFilterResult(int visible, int total) {
|
||||||
|
this->visible = visible;
|
||||||
|
this->total = total;
|
||||||
|
|
||||||
QString result_of_text = tr("of");
|
QString result_of_text = tr("of");
|
||||||
QString result_text;
|
QString result_text;
|
||||||
if (total == 1) {
|
if (total == 1) {
|
||||||
|
@ -108,6 +100,25 @@ void GameList::SearchField::setFilterResult(int visible, int total) {
|
||||||
QString("%1 %2 %3 %4").arg(visible).arg(result_of_text).arg(total).arg(result_text));
|
QString("%1 %2 %3 %4").arg(visible).arg(result_of_text).arg(total).arg(result_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString GameList::getLastFilterResultItem() {
|
||||||
|
QStandardItem* folder;
|
||||||
|
QStandardItem* child;
|
||||||
|
QString file_path;
|
||||||
|
int folderCount = item_model->rowCount();
|
||||||
|
for (int i = 0; i < folderCount; ++i) {
|
||||||
|
folder = item_model->item(i, 0);
|
||||||
|
QModelIndex folder_index = folder->index();
|
||||||
|
int childrenCount = folder->rowCount();
|
||||||
|
for (int j = 0; j < childrenCount; ++j) {
|
||||||
|
if (!tree_view->isRowHidden(j, folder_index)) {
|
||||||
|
child = folder->child(j, 0);
|
||||||
|
file_path = child->data(GameListItemPath::FullPathRole).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file_path;
|
||||||
|
}
|
||||||
|
|
||||||
void GameList::SearchField::clear() {
|
void GameList::SearchField::clear() {
|
||||||
edit_filter->setText("");
|
edit_filter->setText("");
|
||||||
}
|
}
|
||||||
|
@ -161,45 +172,91 @@ bool GameList::containsAllWords(QString haystack, QString userinput) {
|
||||||
[haystack](QString s) { return haystack.contains(s); });
|
[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
|
// Event in order to filter the gamelist after editing the searchfield
|
||||||
void GameList::onTextChanged(const QString& newText) {
|
void GameList::onTextChanged(const QString& newText) {
|
||||||
int rowCount = tree_view->model()->rowCount();
|
int folderCount = tree_view->model()->rowCount();
|
||||||
QString edit_filter_text = newText.toLower();
|
QString edit_filter_text = newText.toLower();
|
||||||
|
QStandardItem* folder;
|
||||||
|
QStandardItem* child;
|
||||||
|
int childrenTotal = 0;
|
||||||
QModelIndex root_index = item_model->invisibleRootItem()->index();
|
QModelIndex root_index = item_model->invisibleRootItem()->index();
|
||||||
|
|
||||||
// If the searchfield is empty every item is visible
|
// If the searchfield is empty every item is visible
|
||||||
// Otherwise the filter gets applied
|
// Otherwise the filter gets applied
|
||||||
if (edit_filter_text.isEmpty()) {
|
if (edit_filter_text.isEmpty()) {
|
||||||
for (int i = 0; i < rowCount; ++i) {
|
for (int i = 0; i < folderCount; ++i) {
|
||||||
tree_view->setRowHidden(i, root_index, false);
|
folder = item_model->item(i, 0);
|
||||||
|
QModelIndex folder_index = folder->index();
|
||||||
|
int childrenCount = folder->rowCount();
|
||||||
|
for (int j = 0; j < childrenCount; ++j) {
|
||||||
|
++childrenTotal;
|
||||||
|
tree_view->setRowHidden(j, folder_index, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
search_field->setFilterResult(rowCount, rowCount);
|
search_field->setFilterResult(childrenTotal, childrenTotal);
|
||||||
} else {
|
} else {
|
||||||
QStandardItem* child_file;
|
|
||||||
QString file_path, file_name, file_title, file_programmid;
|
QString file_path, file_name, file_title, file_programmid;
|
||||||
int result_count = 0;
|
int result_count = 0;
|
||||||
for (int i = 0; i < rowCount; ++i) {
|
for (int i = 0; i < folderCount; ++i) {
|
||||||
child_file = item_model->item(i, 0);
|
folder = item_model->item(i, 0);
|
||||||
file_path = child_file->data(GameListItemPath::FullPathRole).toString().toLower();
|
QModelIndex folder_index = folder->index();
|
||||||
file_name = file_path.mid(file_path.lastIndexOf("/") + 1);
|
int childrenCount = folder->rowCount();
|
||||||
file_title = child_file->data(GameListItemPath::TitleRole).toString().toLower();
|
for (int j = 0; j < childrenCount; ++j) {
|
||||||
file_programmid =
|
++childrenTotal;
|
||||||
child_file->data(GameListItemPath::ProgramIdRole).toString().toLower();
|
child = folder->child(j, 0);
|
||||||
|
file_path = child->data(GameListItemPath::FullPathRole).toString().toLower();
|
||||||
|
file_name = file_path.mid(file_path.lastIndexOf("/") + 1);
|
||||||
|
file_title = child->data(GameListItemPath::TitleRole).toString().toLower();
|
||||||
|
file_programmid = child->data(GameListItemPath::ProgramIdRole).toString().toLower();
|
||||||
|
|
||||||
// Only items which filename in combination with its title contains all words
|
// Only items which filename in combination with its title contains all words
|
||||||
// that are in the searchfield will be visible in the gamelist
|
// that are in the searchfield will be visible in the gamelist
|
||||||
// The search is case insensitive because of toLower()
|
// The search is case insensitive because of toLower()
|
||||||
// I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
|
// I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
|
||||||
// multiple conversions of edit_filter_text for each game in the gamelist
|
// multiple conversions of edit_filter_text for each game in the gamelist
|
||||||
if (containsAllWords(file_name.append(" ").append(file_title), edit_filter_text) ||
|
if (containsAllWords(file_name.append(" ").append(file_title), edit_filter_text) ||
|
||||||
(file_programmid.count() == 16 && edit_filter_text.contains(file_programmid))) {
|
(file_programmid.count() == 16 && edit_filter_text.contains(file_programmid))) {
|
||||||
tree_view->setRowHidden(i, root_index, false);
|
tree_view->setRowHidden(j, folder_index, false);
|
||||||
++result_count;
|
++result_count;
|
||||||
} else {
|
} else {
|
||||||
tree_view->setRowHidden(i, root_index, true);
|
tree_view->setRowHidden(j, folder_index, true);
|
||||||
|
}
|
||||||
|
search_field->setFilterResult(result_count, childrenTotal);
|
||||||
}
|
}
|
||||||
search_field->setFilterResult(result_count, rowCount);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,12 +292,16 @@ GameList::GameList(GMainWindow* parent) : QWidget{parent} {
|
||||||
item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, "Region");
|
item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, "Region");
|
||||||
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
|
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
|
||||||
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size");
|
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::activated, this, &GameList::ValidateEntry);
|
||||||
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
||||||
|
connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
|
||||||
|
connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
|
||||||
|
|
||||||
// We must register all custom types with the Qt Automoc system so that we are able to use it
|
// We must register all custom types with the Qt Automoc system so that we are able to use
|
||||||
// with signals/slots. In this case, QList falls under the umbrells of custom types.
|
// it with signals/slots. In this case, QList falls under the umbrells of custom types.
|
||||||
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
|
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
|
||||||
|
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
@ -268,27 +329,57 @@ void GameList::clearFilter() {
|
||||||
search_field->clear();
|
search_field->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
|
void GameList::AddDirEntry(GameListDir* entry_items) {
|
||||||
item_model->invisibleRootItem()->appendRow(entry_items);
|
item_model->invisibleRootItem()->appendRow(entry_items);
|
||||||
|
tree_view->setExpanded(
|
||||||
|
entry_items->index(),
|
||||||
|
entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
|
||||||
|
parent->appendRow(entry_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::ValidateEntry(const QModelIndex& item) {
|
void GameList::ValidateEntry(const QModelIndex& item) {
|
||||||
// We don't care about the individual QStandardItem that was selected, but its row.
|
auto selected = item.sibling(item.row(), 0);
|
||||||
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();
|
|
||||||
|
|
||||||
if (file_path.isEmpty())
|
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||||
return;
|
case GameListItemType::Game: {
|
||||||
std::string std_file_path(file_path.toStdString());
|
QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
|
||||||
if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
|
if (file_path.isEmpty())
|
||||||
return;
|
return;
|
||||||
// Users usually want to run a diffrent game after closing one
|
QFileInfo file_info(file_path);
|
||||||
search_field->clear();
|
if (!file_info.exists() || file_info.isDir())
|
||||||
emit GameChosen(file_path);
|
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) {
|
void GameList::DonePopulating(QStringList watch_list) {
|
||||||
|
emit ShowList(!isEmpty());
|
||||||
|
|
||||||
|
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
|
||||||
|
|
||||||
// Clear out the old directories to watch for changes and add the new ones
|
// Clear out the old directories to watch for changes and add the new ones
|
||||||
auto watch_dirs = watcher->directories();
|
auto watch_dirs = watcher->directories();
|
||||||
if (!watch_dirs.isEmpty()) {
|
if (!watch_dirs.isEmpty()) {
|
||||||
|
@ -305,9 +396,16 @@ void GameList::DonePopulating(QStringList watch_list) {
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
}
|
}
|
||||||
tree_view->setEnabled(true);
|
tree_view->setEnabled(true);
|
||||||
int rowCount = tree_view->model()->rowCount();
|
int folderCount = tree_view->model()->rowCount();
|
||||||
search_field->setFilterResult(rowCount, rowCount);
|
int childrenTotal = 0;
|
||||||
if (rowCount > 0) {
|
for (int i = 0; i < folderCount; ++i) {
|
||||||
|
int childrenCount = item_model->item(i, 0)->rowCount();
|
||||||
|
for (int j = 0; j < childrenCount; ++j) {
|
||||||
|
++childrenTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
search_field->setFilterResult(childrenTotal, childrenTotal);
|
||||||
|
if (childrenTotal > 0) {
|
||||||
search_field->setFocus();
|
search_field->setFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -317,12 +415,25 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||||
if (!item.isValid())
|
if (!item.isValid())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int row = item_model->itemFromIndex(item)->row();
|
auto selected = item.sibling(item.row(), 0);
|
||||||
QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
|
|
||||||
u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong();
|
|
||||||
|
|
||||||
QMenu context_menu;
|
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_save_location = context_menu.addAction(tr("Open Save Data Location"));
|
||||||
QAction* open_application_location = context_menu.addAction(tr("Open Application Location"));
|
QAction* open_application_location = context_menu.addAction(tr("Open Application Location"));
|
||||||
QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location"));
|
QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location"));
|
||||||
|
@ -341,16 +452,81 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||||
});
|
});
|
||||||
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
|
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
|
||||||
|
|
||||||
connect(open_save_location, &QAction::triggered,
|
connect(open_save_location, &QAction::triggered, [this, program_id] {
|
||||||
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); });
|
emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA);
|
||||||
connect(open_application_location, &QAction::triggered,
|
});
|
||||||
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); });
|
connect(open_application_location, &QAction::triggered, [this, program_id] {
|
||||||
connect(open_update_location, &QAction::triggered,
|
emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION);
|
||||||
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); });
|
});
|
||||||
connect(navigate_to_gamedb_entry, &QAction::triggered,
|
connect(open_update_location, &QAction::triggered, [this, program_id] {
|
||||||
[&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); });
|
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() {
|
void GameList::LoadCompatibilityList() {
|
||||||
|
@ -399,27 +575,23 @@ QStandardItemModel* GameList::GetModel() const {
|
||||||
return item_model;
|
return item_model;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
|
void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
tree_view->setEnabled(false);
|
tree_view->setEnabled(false);
|
||||||
// Delete any rows that might already exist if we're repopulating
|
// Delete any rows that might already exist if we're repopulating
|
||||||
item_model->removeRows(0, item_model->rowCount());
|
item_model->removeRows(0, item_model->rowCount());
|
||||||
|
search_field->clear();
|
||||||
|
|
||||||
emit ShouldCancelWorker();
|
emit ShouldCancelWorker();
|
||||||
|
|
||||||
GameListWorker* worker = 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::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
|
||||||
|
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
|
||||||
|
Qt::QueuedConnection);
|
||||||
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
|
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel
|
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
|
||||||
// without delay.
|
// cancel without delay.
|
||||||
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
|
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
|
||||||
Qt::DirectConnection);
|
Qt::DirectConnection);
|
||||||
|
|
||||||
|
@ -451,16 +623,17 @@ static bool HasSupportedFileExtension(const std::string& file_name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::RefreshGameDirectory() {
|
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.");
|
NGLOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||||
search_field->clear();
|
PopulateAsync(UISettings::values.game_dirs);
|
||||||
PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
|
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
|
||||||
const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
|
GameListDir* parent_dir) {
|
||||||
const std::string& virtual_name) -> bool {
|
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;
|
std::string physical_name = directory + DIR_SEP + virtual_name;
|
||||||
|
|
||||||
if (stop_processing)
|
if (stop_processing)
|
||||||
|
@ -510,17 +683,20 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
|
||||||
if (it != compatibility_list.end())
|
if (it != compatibility_list.end())
|
||||||
compatibility = it->second.first;
|
compatibility = it->second.first;
|
||||||
|
|
||||||
emit EntryReady({
|
emit EntryReady(
|
||||||
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
|
{
|
||||||
new GameListItemCompat(compatibility),
|
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
|
||||||
new GameListItemRegion(smdh),
|
new GameListItemCompat(compatibility),
|
||||||
new GameListItem(
|
new GameListItemRegion(smdh),
|
||||||
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
|
new GameListItem(
|
||||||
new GameListItemSize(FileUtil::GetSize(physical_name)),
|
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
|
||||||
});
|
new GameListItemSize(FileUtil::GetSize(physical_name)),
|
||||||
|
},
|
||||||
|
parent_dir);
|
||||||
|
|
||||||
} else if (is_dir && recursion > 0) {
|
} else if (is_dir && recursion > 0) {
|
||||||
watch_list.append(QString::fromStdString(physical_name));
|
watch_list.append(QString::fromStdString(physical_name));
|
||||||
AddFstEntriesToGameList(physical_name, recursion - 1);
|
AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -531,27 +707,33 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
|
||||||
|
|
||||||
void GameListWorker::run() {
|
void GameListWorker::run() {
|
||||||
stop_processing = false;
|
stop_processing = false;
|
||||||
watch_list.append(dir_path);
|
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||||
watch_list.append(QString::fromStdString(
|
if (game_dir.path == "INSTALLED") {
|
||||||
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
|
QString path = QString(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
|
||||||
"Nintendo "
|
"Nintendo "
|
||||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000"));
|
"3DS/00000000000000000000000000000000/"
|
||||||
watch_list.append(QString::fromStdString(
|
"00000000000000000000000000000000/title/00040000";
|
||||||
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
|
watch_list.append(path);
|
||||||
"Nintendo "
|
GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
|
||||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004000e"));
|
emit DirEntryReady({game_list_dir});
|
||||||
watch_list.append(
|
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir);
|
||||||
QString::fromStdString(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
|
} else if (game_dir.path == "SYSTEM") {
|
||||||
"00000000000000000000000000000000/title/00040010"));
|
QString path = QString(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
|
||||||
AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
|
"00000000000000000000000000000000/title/00040010";
|
||||||
AddFstEntriesToGameList(
|
watch_list.append(path);
|
||||||
std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
|
GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
|
||||||
"Nintendo "
|
emit DirEntryReady({game_list_dir});
|
||||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000",
|
AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
|
||||||
2);
|
"00000000000000000000000000000000/title/00040010",
|
||||||
AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
|
2, game_list_dir);
|
||||||
"00000000000000000000000000000000/title/00040010",
|
} else {
|
||||||
2);
|
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);
|
emit Finished(watch_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,3 +741,37 @@ void GameListWorker::Cancel() {
|
||||||
this->disconnect();
|
this->disconnect();
|
||||||
stop_processing = true;
|
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 <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "ui_settings.h"
|
||||||
|
|
||||||
class GameListWorker;
|
class GameListWorker;
|
||||||
|
class GameListDir;
|
||||||
class GMainWindow;
|
class GMainWindow;
|
||||||
class QFileSystemWatcher;
|
class QFileSystemWatcher;
|
||||||
class QHBoxLayout;
|
class QHBoxLayout;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
template <typename>
|
||||||
|
class QList;
|
||||||
class QModelIndex;
|
class QModelIndex;
|
||||||
class QStandardItem;
|
class QStandardItem;
|
||||||
class QStandardItemModel;
|
class QStandardItemModel;
|
||||||
|
@ -39,10 +43,14 @@ public:
|
||||||
|
|
||||||
class SearchField : public QWidget {
|
class SearchField : public QWidget {
|
||||||
public:
|
public:
|
||||||
|
explicit SearchField(GameList* parent = nullptr);
|
||||||
|
|
||||||
void setFilterResult(int visible, int total);
|
void setFilterResult(int visible, int total);
|
||||||
void clear();
|
void clear();
|
||||||
void setFocus();
|
void setFocus();
|
||||||
explicit SearchField(GameList* parent = nullptr);
|
|
||||||
|
int visible;
|
||||||
|
int total;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class KeyReleaseEater : public QObject {
|
class KeyReleaseEater : public QObject {
|
||||||
|
@ -67,12 +75,14 @@ public:
|
||||||
explicit GameList(GMainWindow* parent = nullptr);
|
explicit GameList(GMainWindow* parent = nullptr);
|
||||||
~GameList() override;
|
~GameList() override;
|
||||||
|
|
||||||
|
QString getLastFilterResultItem();
|
||||||
void clearFilter();
|
void clearFilter();
|
||||||
void setFilterFocus();
|
void setFilterFocus();
|
||||||
void setFilterVisible(bool visibility);
|
void setFilterVisible(bool visibility);
|
||||||
|
bool isEmpty();
|
||||||
|
|
||||||
void LoadCompatibilityList();
|
void LoadCompatibilityList();
|
||||||
void PopulateAsync(const QString& dir_path, bool deep_scan);
|
void PopulateAsync(QList<UISettings::GameDir>& game_dirs);
|
||||||
|
|
||||||
void SaveInterfaceLayout();
|
void SaveInterfaceLayout();
|
||||||
void LoadInterfaceLayout();
|
void LoadInterfaceLayout();
|
||||||
|
@ -88,20 +98,30 @@ signals:
|
||||||
void NavigateToGamedbEntryRequested(
|
void NavigateToGamedbEntryRequested(
|
||||||
u64 program_id,
|
u64 program_id,
|
||||||
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
|
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
|
||||||
|
void OpenDirectory(QString directory);
|
||||||
|
void AddDirectory();
|
||||||
|
void ShowList(bool show);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void onItemExpanded(const QModelIndex& item);
|
||||||
void onTextChanged(const QString& newText);
|
void onTextChanged(const QString& newText);
|
||||||
void onFilterCloseClicked();
|
void onFilterCloseClicked();
|
||||||
|
void onUpdateThemedIcons();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AddEntry(const QList<QStandardItem*>& entry_items);
|
void AddDirEntry(GameListDir* entry_items);
|
||||||
|
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
|
||||||
void ValidateEntry(const QModelIndex& item);
|
void ValidateEntry(const QModelIndex& item);
|
||||||
void DonePopulating(QStringList watch_list);
|
void DonePopulating(QStringList watch_list);
|
||||||
|
|
||||||
void PopupContextMenu(const QPoint& menu_location);
|
|
||||||
void RefreshGameDirectory();
|
void RefreshGameDirectory();
|
||||||
bool containsAllWords(QString haystack, QString userinput);
|
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;
|
SearchField* search_field;
|
||||||
GMainWindow* main_window = nullptr;
|
GMainWindow* main_window = nullptr;
|
||||||
QVBoxLayout* layout = nullptr;
|
QVBoxLayout* layout = nullptr;
|
||||||
|
@ -113,3 +133,25 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(GameListOpenTarget);
|
Q_DECLARE_METATYPE(GameListOpenTarget);
|
||||||
|
|
||||||
|
class GameListPlaceholder : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit GameListPlaceholder(GMainWindow* parent = nullptr);
|
||||||
|
~GameListPlaceholder();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void AddDirectory();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onUpdateThemedIcons();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GMainWindow* main_window = nullptr;
|
||||||
|
QVBoxLayout* layout = nullptr;
|
||||||
|
QLabel* image = nullptr;
|
||||||
|
QLabel* text = nullptr;
|
||||||
|
};
|
||||||
|
|
|
@ -8,18 +8,30 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
#include "citra_qt/util/util.h"
|
#include "citra_qt/util/util.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "core/loader/smdh.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.
|
* Gets the game icon from SMDH data.
|
||||||
* @param smdh SMDH data
|
* @param smdh SMDH data
|
||||||
|
@ -126,8 +138,13 @@ const static inline std::map<QString, CompatStatus> status_data = {
|
||||||
|
|
||||||
class GameListItem : public QStandardItem {
|
class GameListItem : public QStandardItem {
|
||||||
public:
|
public:
|
||||||
|
// used to access type from item index
|
||||||
|
static const int TypeRole = Qt::UserRole + 1;
|
||||||
|
static const int SortRole = Qt::UserRole + 2;
|
||||||
GameListItem() : QStandardItem() {}
|
GameListItem() : QStandardItem() {}
|
||||||
GameListItem(const QString& string) : QStandardItem(string) {}
|
GameListItem(const QString& string) : QStandardItem(string) {
|
||||||
|
setData(string, SortRole);
|
||||||
|
}
|
||||||
virtual ~GameListItem() override {}
|
virtual ~GameListItem() override {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,13 +156,14 @@ public:
|
||||||
*/
|
*/
|
||||||
class GameListItemPath : public GameListItem {
|
class GameListItemPath : public GameListItem {
|
||||||
public:
|
public:
|
||||||
static const int FullPathRole = Qt::UserRole + 1;
|
static const int TitleRole = SortRole;
|
||||||
static const int TitleRole = Qt::UserRole + 2;
|
static const int FullPathRole = SortRole + 1;
|
||||||
static const int ProgramIdRole = Qt::UserRole + 3;
|
static const int ProgramIdRole = SortRole + 2;
|
||||||
|
|
||||||
GameListItemPath() : GameListItem() {}
|
GameListItemPath() : GameListItem() {}
|
||||||
GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id)
|
GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id)
|
||||||
: GameListItem() {
|
: GameListItem() {
|
||||||
|
setData(type(), TypeRole);
|
||||||
setData(game_path, FullPathRole);
|
setData(game_path, FullPathRole);
|
||||||
setData(qulonglong(program_id), ProgramIdRole);
|
setData(qulonglong(program_id), ProgramIdRole);
|
||||||
|
|
||||||
|
@ -166,6 +184,10 @@ public:
|
||||||
TitleRole);
|
TitleRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int type() const override {
|
||||||
|
return static_cast<int>(GameListItemType::Game);
|
||||||
|
}
|
||||||
|
|
||||||
QVariant data(int role) const override {
|
QVariant data(int role) const override {
|
||||||
if (role == Qt::DisplayRole) {
|
if (role == Qt::DisplayRole) {
|
||||||
std::string path, filename, extension;
|
std::string path, filename, extension;
|
||||||
|
@ -202,9 +224,12 @@ public:
|
||||||
|
|
||||||
class GameListItemCompat : public GameListItem {
|
class GameListItemCompat : public GameListItem {
|
||||||
public:
|
public:
|
||||||
static const int CompatNumberRole = Qt::UserRole + 1;
|
static const int CompatNumberRole = SortRole;
|
||||||
|
|
||||||
GameListItemCompat() = default;
|
GameListItemCompat() = default;
|
||||||
explicit GameListItemCompat(const QString compatiblity) {
|
explicit GameListItemCompat(const QString compatiblity) {
|
||||||
|
setData(type(), TypeRole);
|
||||||
|
|
||||||
auto iterator = status_data.find(compatiblity);
|
auto iterator = status_data.find(compatiblity);
|
||||||
if (iterator == status_data.end()) {
|
if (iterator == status_data.end()) {
|
||||||
NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString());
|
NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString());
|
||||||
|
@ -217,6 +242,10 @@ public:
|
||||||
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int type() const override {
|
||||||
|
return static_cast<int>(GameListItemType::Game);
|
||||||
|
}
|
||||||
|
|
||||||
bool operator<(const QStandardItem& other) const override {
|
bool operator<(const QStandardItem& other) const override {
|
||||||
return data(CompatNumberRole) < other.data(CompatNumberRole);
|
return data(CompatNumberRole) < other.data(CompatNumberRole);
|
||||||
}
|
}
|
||||||
|
@ -226,6 +255,8 @@ class GameListItemRegion : public GameListItem {
|
||||||
public:
|
public:
|
||||||
GameListItemRegion() = default;
|
GameListItemRegion() = default;
|
||||||
explicit GameListItemRegion(const std::vector<u8>& smdh_data) {
|
explicit GameListItemRegion(const std::vector<u8>& smdh_data) {
|
||||||
|
setData(type(), TypeRole);
|
||||||
|
|
||||||
if (!Loader::IsValidSMDH(smdh_data)) {
|
if (!Loader::IsValidSMDH(smdh_data)) {
|
||||||
setText(QObject::tr("Invalid region"));
|
setText(QObject::tr("Invalid region"));
|
||||||
return;
|
return;
|
||||||
|
@ -235,6 +266,11 @@ public:
|
||||||
memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
|
memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
|
||||||
|
|
||||||
setText(GetRegionFromSMDH(smdh));
|
setText(GetRegionFromSMDH(smdh));
|
||||||
|
setData(GetRegionFromSMDH(smdh), SortRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
int type() const override {
|
||||||
|
return static_cast<int>(GameListItemType::Game);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -245,10 +281,11 @@ public:
|
||||||
*/
|
*/
|
||||||
class GameListItemSize : public GameListItem {
|
class GameListItemSize : public GameListItem {
|
||||||
public:
|
public:
|
||||||
static const int SizeRole = Qt::UserRole + 1;
|
static const int SizeRole = SortRole;
|
||||||
|
|
||||||
GameListItemSize() : GameListItem() {}
|
GameListItemSize() : GameListItem() {}
|
||||||
GameListItemSize(const qulonglong size_bytes) : GameListItem() {
|
GameListItemSize(const qulonglong size_bytes) : GameListItem() {
|
||||||
|
setData(type(), TypeRole);
|
||||||
setData(size_bytes, SizeRole);
|
setData(size_bytes, SizeRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +301,10 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int type() const override {
|
||||||
|
return static_cast<int>(GameListItemType::Game);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This operator is, in practice, only used by the TreeView sorting systems.
|
* This operator is, in practice, only used by the TreeView sorting systems.
|
||||||
* Override it so that it will correctly sort by numerical value instead of by string
|
* Override it so that it will correctly sort by numerical value instead of by string
|
||||||
|
@ -274,6 +315,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.
|
* Asynchronous worker object for populating the game list.
|
||||||
* Communicates with other threads through Qt's signal/slot system.
|
* Communicates with other threads through Qt's signal/slot system.
|
||||||
|
@ -282,11 +372,10 @@ class GameListWorker : public QObject, public QRunnable {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GameListWorker(
|
explicit GameListWorker(
|
||||||
QString dir_path, bool deep_scan,
|
QList<UISettings::GameDir>& game_dirs,
|
||||||
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list)
|
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list)
|
||||||
: QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan),
|
: QObject(), QRunnable(), game_dirs(game_dirs), compatibility_list(compatibility_list) {}
|
||||||
compatibility_list(compatibility_list) {}
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/// Starts the processing of directory tree information.
|
/// Starts the processing of directory tree information.
|
||||||
|
@ -298,22 +387,24 @@ signals:
|
||||||
/**
|
/**
|
||||||
* The `EntryReady` signal is emitted once an entry has been prepared and is ready
|
* The `EntryReady` signal is emitted once an entry has been prepared and is ready
|
||||||
* to be added to the game list.
|
* to be added to the game list.
|
||||||
* @param entry_items a list with `QStandardItem`s that make up the columns of the new entry.
|
* @param entry_items a list with `QStandardItem`s that make up the columns of the new
|
||||||
|
* entry.
|
||||||
*/
|
*/
|
||||||
void EntryReady(QList<QStandardItem*> entry_items);
|
void DirEntryReady(GameListDir* entry_items);
|
||||||
|
void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After the worker has traversed the game directory looking for entries, this signal is emmited
|
* After the worker has traversed the game directory looking for entries, this signal is
|
||||||
* with a list of folders that should be watched for changes as well.
|
* emitted with a list of folders that should be watched for changes as well.
|
||||||
*/
|
*/
|
||||||
void Finished(QStringList watch_list);
|
void Finished(QStringList watch_list);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QStringList watch_list;
|
QStringList watch_list;
|
||||||
QString dir_path;
|
|
||||||
bool deep_scan;
|
|
||||||
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
|
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
|
||||||
|
QList<UISettings::GameDir>& game_dirs;
|
||||||
std::atomic_bool stop_processing;
|
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();
|
show();
|
||||||
|
|
||||||
game_list->LoadCompatibilityList();
|
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
|
// Show one-time "callout" messages to the user
|
||||||
ShowCallouts();
|
ShowCallouts();
|
||||||
|
@ -177,6 +177,10 @@ void GMainWindow::InitializeWidgets() {
|
||||||
game_list = new GameList(this);
|
game_list = new GameList(this);
|
||||||
ui.horizontalLayout->addWidget(game_list);
|
ui.horizontalLayout->addWidget(game_list);
|
||||||
|
|
||||||
|
game_list_placeholder = new GameListPlaceholder(this);
|
||||||
|
ui.horizontalLayout->addWidget(game_list_placeholder);
|
||||||
|
game_list_placeholder->setVisible(false);
|
||||||
|
|
||||||
multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
|
multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
|
||||||
ui.action_Show_Room);
|
ui.action_Show_Room);
|
||||||
multiplayer_state->setVisible(false);
|
multiplayer_state->setVisible(false);
|
||||||
|
@ -415,9 +419,14 @@ void GMainWindow::RestoreUIState() {
|
||||||
|
|
||||||
void GMainWindow::ConnectWidgetEvents() {
|
void GMainWindow::ConnectWidgetEvents() {
|
||||||
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
|
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
|
||||||
|
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
|
||||||
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
|
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
|
||||||
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
|
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
|
||||||
&GMainWindow::OnGameListNavigateToGamedbEntry);
|
&GMainWindow::OnGameListNavigateToGamedbEntry);
|
||||||
|
connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
|
||||||
|
connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
|
||||||
|
&GMainWindow::OnGameListAddDirectory);
|
||||||
|
connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
|
||||||
|
|
||||||
connect(this, &GMainWindow::EmulationStarting, render_window,
|
connect(this, &GMainWindow::EmulationStarting, render_window,
|
||||||
&GRenderWindow::OnEmulationStarting);
|
&GRenderWindow::OnEmulationStarting);
|
||||||
|
@ -435,8 +444,6 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
// File
|
// File
|
||||||
connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
|
connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
|
||||||
connect(ui.action_Install_CIA, &QAction::triggered, this, &GMainWindow::OnMenuInstallCIA);
|
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);
|
connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
|
||||||
|
|
||||||
// Emulation
|
// Emulation
|
||||||
|
@ -688,6 +695,7 @@ void GMainWindow::BootGame(const QString& filename) {
|
||||||
registersWidget->OnDebugModeEntered();
|
registersWidget->OnDebugModeEntered();
|
||||||
if (ui.action_Single_Window_Mode->isChecked()) {
|
if (ui.action_Single_Window_Mode->isChecked()) {
|
||||||
game_list->hide();
|
game_list->hide();
|
||||||
|
game_list_placeholder->hide();
|
||||||
}
|
}
|
||||||
status_bar_update_timer.start(2000);
|
status_bar_update_timer.start(2000);
|
||||||
|
|
||||||
|
@ -729,7 +737,10 @@ void GMainWindow::ShutdownGame() {
|
||||||
ui.action_Stop->setEnabled(false);
|
ui.action_Stop->setEnabled(false);
|
||||||
ui.action_Report_Compatibility->setEnabled(false);
|
ui.action_Report_Compatibility->setEnabled(false);
|
||||||
render_window->hide();
|
render_window->hide();
|
||||||
game_list->show();
|
if (game_list->isEmpty())
|
||||||
|
game_list_placeholder->show();
|
||||||
|
else
|
||||||
|
game_list->show();
|
||||||
game_list->setFilterFocus();
|
game_list->setFilterFocus();
|
||||||
|
|
||||||
// Disable status bar updates
|
// Disable status bar updates
|
||||||
|
@ -844,6 +855,48 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(
|
||||||
QDesktopServices::openUrl(QUrl("https://citra-emu.org/game/" + directory));
|
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() {
|
void GMainWindow::OnMenuLoadFile() {
|
||||||
QString extensions;
|
QString extensions;
|
||||||
for (const auto& piece : game_list->supported_file_extensions)
|
for (const auto& piece : game_list->supported_file_extensions)
|
||||||
|
@ -861,14 +914,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() {
|
void GMainWindow::OnMenuInstallCIA() {
|
||||||
QStringList filepaths = QFileDialog::getOpenFileNames(
|
QStringList filepaths = QFileDialog::getOpenFileNames(
|
||||||
this, tr("Load Files"), UISettings::values.roms_path,
|
this, tr("Load Files"), UISettings::values.roms_path,
|
||||||
|
@ -1105,6 +1150,7 @@ void GMainWindow::OnConfigure() {
|
||||||
if (result == QDialog::Accepted) {
|
if (result == QDialog::Accepted) {
|
||||||
configureDialog.applyConfiguration();
|
configureDialog.applyConfiguration();
|
||||||
UpdateUITheme();
|
UpdateUITheme();
|
||||||
|
emit UpdateThemedIcons();
|
||||||
SyncMenuUISettings();
|
SyncMenuUISettings();
|
||||||
config->Save();
|
config->Save();
|
||||||
}
|
}
|
||||||
|
@ -1324,7 +1370,6 @@ void GMainWindow::UpdateUITheme() {
|
||||||
QIcon::setThemeName(":/icons/default");
|
QIcon::setThemeName(":/icons/default");
|
||||||
}
|
}
|
||||||
QIcon::setThemeSearchPaths(theme_paths);
|
QIcon::setThemeSearchPaths(theme_paths);
|
||||||
emit UpdateThemedIcons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::LoadTranslation() {
|
void GMainWindow::LoadTranslation() {
|
||||||
|
|
|
@ -20,6 +20,7 @@ class ClickableLabel;
|
||||||
class EmuThread;
|
class EmuThread;
|
||||||
class GameList;
|
class GameList;
|
||||||
enum class GameListOpenTarget;
|
enum class GameListOpenTarget;
|
||||||
|
class GameListPlaceholder;
|
||||||
class GImageInfo;
|
class GImageInfo;
|
||||||
class GPUCommandListWidget;
|
class GPUCommandListWidget;
|
||||||
class GPUCommandStreamWidget;
|
class GPUCommandStreamWidget;
|
||||||
|
@ -148,13 +149,14 @@ private slots:
|
||||||
void OnGameListNavigateToGamedbEntry(
|
void OnGameListNavigateToGamedbEntry(
|
||||||
u64 program_id,
|
u64 program_id,
|
||||||
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
|
std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
|
||||||
|
void OnGameListOpenDirectory(QString path);
|
||||||
|
void OnGameListAddDirectory();
|
||||||
|
void OnGameListShowList(bool show);
|
||||||
void OnMenuLoadFile();
|
void OnMenuLoadFile();
|
||||||
void OnMenuInstallCIA();
|
void OnMenuInstallCIA();
|
||||||
void OnUpdateProgress(size_t written, size_t total);
|
void OnUpdateProgress(size_t written, size_t total);
|
||||||
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
|
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
|
||||||
void OnCIAInstallFinished();
|
void OnCIAInstallFinished();
|
||||||
/// Called whenever a user selects the "File->Select Game List Root" menu item
|
|
||||||
void OnMenuSelectGameListRoot();
|
|
||||||
void OnMenuRecentFile();
|
void OnMenuRecentFile();
|
||||||
void OnConfigure();
|
void OnConfigure();
|
||||||
void OnToggleFilterBar();
|
void OnToggleFilterBar();
|
||||||
|
@ -184,6 +186,8 @@ private:
|
||||||
|
|
||||||
GRenderWindow* render_window;
|
GRenderWindow* render_window;
|
||||||
|
|
||||||
|
GameListPlaceholder* game_list_placeholder;
|
||||||
|
|
||||||
// Status bar elements
|
// Status bar elements
|
||||||
QProgressBar* progress_bar = nullptr;
|
QProgressBar* progress_bar = nullptr;
|
||||||
QLabel* message_label = nullptr;
|
QLabel* message_label = nullptr;
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
<addaction name="action_Load_File"/>
|
<addaction name="action_Load_File"/>
|
||||||
<addaction name="action_Install_CIA"/>
|
<addaction name="action_Install_CIA"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_Select_Game_List_Root"/>
|
|
||||||
<addaction name="menu_recent_files"/>
|
<addaction name="menu_recent_files"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_Exit"/>
|
<addaction name="action_Exit"/>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
|
HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
|
||||||
std::shared_ptr<Core::AnnounceMultiplayerSession> session)
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session)
|
||||||
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
||||||
ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session), game_list(list) {
|
ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session) {
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
|
||||||
// set up validation for all of the fields
|
// set up validation for all of the fields
|
||||||
|
@ -35,6 +35,15 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
|
||||||
ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
|
ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
|
||||||
|
|
||||||
// Create a proxy to the game list to display the list of preferred games
|
// Create a proxy to the game list to display the list of preferred games
|
||||||
|
game_list = new QStandardItemModel;
|
||||||
|
|
||||||
|
for (int i = 0; i < list->rowCount(); i++) {
|
||||||
|
auto parent = list->item(i, 0);
|
||||||
|
for (int j = 0; j < parent->rowCount(); j++) {
|
||||||
|
game_list->appendRow(parent->child(j)->clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
proxy = new ComboBoxProxyModel;
|
proxy = new ComboBoxProxyModel;
|
||||||
proxy->setSourceModel(game_list);
|
proxy->setSourceModel(game_list);
|
||||||
proxy->sort(0, Qt::AscendingOrder);
|
proxy->sort(0, Qt::AscendingOrder);
|
||||||
|
@ -152,8 +161,7 @@ QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
|
bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
|
||||||
// TODO(jroweboy): Sort by game title not filename
|
auto leftData = left.data(GameListItemPath::TitleRole).toString();
|
||||||
auto leftData = left.data(Qt::DisplayRole).toString();
|
auto rightData = right.data(GameListItemPath::TitleRole).toString();
|
||||||
auto rightData = right.data(Qt::DisplayRole).toString();
|
|
||||||
return leftData.compare(rightData) < 0;
|
return leftData.compare(rightData) < 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QMetaType>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#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("Default"), QString("default")),
|
||||||
std::make_pair(QString("Dark"), QString("qdarkstyle"))}};
|
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 {
|
struct Values {
|
||||||
QByteArray geometry;
|
QByteArray geometry;
|
||||||
QByteArray state;
|
QByteArray state;
|
||||||
|
@ -45,8 +58,9 @@ struct Values {
|
||||||
|
|
||||||
QString roms_path;
|
QString roms_path;
|
||||||
QString symbols_path;
|
QString symbols_path;
|
||||||
QString gamedir;
|
QString game_dir_deprecated;
|
||||||
bool gamedir_deepscan;
|
bool game_dir_deprecated_deepscan;
|
||||||
|
QList<UISettings::GameDir> game_dirs;
|
||||||
QStringList recent_files;
|
QStringList recent_files;
|
||||||
QString language;
|
QString language;
|
||||||
|
|
||||||
|
@ -74,3 +88,5 @@ struct Values {
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
} // namespace UISettings
|
} // namespace UISettings
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(UISettings::GameDir*);
|
||||||
|
|