citra_qt: support 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 | 
							
								
								
									
										5
									
								
								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> | ||||||
|  |  | ||||||
|  | @ -202,8 +202,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(); | ||||||
|  | @ -378,8 +404,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> | ||||||
|  |  | ||||||
|  | @ -66,6 +66,7 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e | ||||||
|         case Qt::Key_Enter: { |         case Qt::Key_Enter: { | ||||||
|             if (gamelist->search_field->visible == 1) { |             if (gamelist->search_field->visible == 1) { | ||||||
|                 QString file_path = gamelist->getLastFilterResultItem(); |                 QString file_path = gamelist->getLastFilterResultItem(); | ||||||
|  | 
 | ||||||
|                 // 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(""); | ||||||
|  | @ -171,6 +172,15 @@ 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 folderCount = tree_view->model()->rowCount(); |     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() { | void GameList::onFilterCloseClicked() { | ||||||
|     main_window->filterBarSetChecked(false); |     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_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); | ||||||
|  | @ -290,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()) { | ||||||
|  | @ -346,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")); | ||||||
|  | @ -370,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() { | ||||||
|  | @ -428,14 +575,7 @@ 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()); | ||||||
|  | @ -443,13 +583,15 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | ||||||
| 
 | 
 | ||||||
|     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); | ||||||
| 
 | 
 | ||||||
|  | @ -481,15 +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."); | ||||||
|         PopulateAsync(UISettings::values.gamedirs); |         PopulateAsync(UISettings::values.game_dirs); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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) | ||||||
|  | @ -539,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; | ||||||
|  | @ -560,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); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -588,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,12 +43,14 @@ public: | ||||||
| 
 | 
 | ||||||
|     class SearchField : public QWidget { |     class SearchField : public QWidget { | ||||||
|     public: |     public: | ||||||
|         int visible; |         explicit SearchField(GameList* parent = nullptr); | ||||||
|         int total; | 
 | ||||||
|         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 { | ||||||
|  | @ -73,9 +79,10 @@ public: | ||||||
|     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(); | ||||||
|  | @ -91,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; | ||||||
|  | @ -116,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,17 +8,29 @@ | ||||||
| #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/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 | ||||||
|  | @ -125,8 +137,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 {} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -138,13 +155,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); | ||||||
| 
 | 
 | ||||||
|  | @ -165,6 +183,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 filename; |             std::string filename; | ||||||
|  | @ -180,9 +202,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()); | ||||||
|  | @ -195,6 +220,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); | ||||||
|     } |     } | ||||||
|  | @ -204,6 +233,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; | ||||||
|  | @ -213,6 +244,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); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -223,10 +259,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); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -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. |      * 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 | ||||||
|  | @ -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. |  * 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. | ||||||
|  | @ -260,11 +350,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.
 | ||||||
|  | @ -276,22 +365,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); | ||||||
|  | @ -399,9 +403,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); | ||||||
|  | @ -419,8 +428,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
 | ||||||
|  | @ -672,6 +679,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); | ||||||
| 
 | 
 | ||||||
|  | @ -713,7 +721,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
 | ||||||
|  | @ -828,6 +839,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) | ||||||
|  | @ -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() { | 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, | ||||||
|  | @ -1089,6 +1134,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(); | ||||||
|     } |     } | ||||||
|  | @ -1308,7 +1354,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"/> | ||||||
|  |  | ||||||
|  | @ -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*); | ||||||
|  |  | ||||||