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="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> | ||||
|        | ||||
|         <file alias="48x48/chip.png">icons/48x48/chip.png</file> | ||||
| 
 | ||||
|         <file alias="48x48/folder.png">icons/48x48/folder.png</file> | ||||
| 
 | ||||
|         <file alias="48x48/plus.png">icons/48x48/plus.png</file> | ||||
|        | ||||
|         <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> | ||||
|        | ||||
|         <file alias="256x256/citra.png">icons/256x256/citra.png</file> | ||||
| 
 | ||||
|         <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> | ||||
|     </qresource> | ||||
| </RCC> | ||||
|  |  | |||
							
								
								
									
										
											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] | ||||
| Name=default | ||||
| Comment=default theme | ||||
| Directories=16x16,256x256 | ||||
| Directories=16x16,48x48,256x256 | ||||
|   | ||||
| [16x16] | ||||
| Size=16 | ||||
| 
 | ||||
| [48x48] | ||||
| Size=48 | ||||
|   | ||||
| [256x256] | ||||
| 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 | ||||
| Comment=dark theme | ||||
| Inherits=default | ||||
| Directories=16x16,256x256 | ||||
| Directories=16x16,48x48,256x256 | ||||
|   | ||||
| [16x16] | ||||
| Size=16 | ||||
| 
 | ||||
| [48x48] | ||||
| Size=48 | ||||
| 
 | ||||
| [256x256] | ||||
| Size=256 | ||||
							
								
								
									
										6
									
								
								dist/qt_themes/qdarkstyle/style.qrc
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,12 @@ | |||
|   <qresource prefix="icons/qdarkstyle"> | ||||
|     <file alias="index.theme">icons/index.theme</file> | ||||
|     <file alias="16x16/lock.png">icons/16x16/lock.png</file> | ||||
|     <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> | ||||
|     <file alias="48x48/chip.png">icons/48x48/chip.png</file> | ||||
|     <file alias="48x48/folder.png">icons/48x48/folder.png</file> | ||||
|     <file alias="48x48/plus.png">icons/48x48/plus.png</file> | ||||
|     <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> | ||||
|     <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> | ||||
|   </qresource> | ||||
|   <qresource prefix="qss_icons"> | ||||
|     <file>rc/up_arrow_disabled.png</file> | ||||
|  |  | |||
|  | @ -202,8 +202,34 @@ void Config::ReadValues() { | |||
|     qt_config->beginGroup("Paths"); | ||||
|     UISettings::values.roms_path = qt_config->value("romsPath").toString(); | ||||
|     UISettings::values.symbols_path = qt_config->value("symbolsPath").toString(); | ||||
|     UISettings::values.gamedir = qt_config->value("gameListRootDir", ".").toString(); | ||||
|     UISettings::values.gamedir_deepscan = qt_config->value("gameListDeepScan", false).toBool(); | ||||
|     UISettings::values.game_dir_deprecated = qt_config->value("gameListRootDir", ".").toString(); | ||||
|     UISettings::values.game_dir_deprecated_deepscan = | ||||
|         qt_config->value("gameListDeepScan", false).toBool(); | ||||
|     int size = qt_config->beginReadArray("gamedirs"); | ||||
|     for (int i = 0; i < size; ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         UISettings::GameDir game_dir; | ||||
|         game_dir.path = qt_config->value("path").toString(); | ||||
|         game_dir.deep_scan = qt_config->value("deep_scan", false).toBool(); | ||||
|         game_dir.expanded = qt_config->value("expanded", true).toBool(); | ||||
|         UISettings::values.game_dirs.append(game_dir); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     // create NAND and SD card directories if empty, these are not removable through the UI, also
 | ||||
|     // carries over old game list settings if present
 | ||||
|     if (UISettings::values.game_dirs.isEmpty()) { | ||||
|         UISettings::GameDir game_dir; | ||||
|         game_dir.path = "INSTALLED"; | ||||
|         game_dir.expanded = true; | ||||
|         UISettings::values.game_dirs.append(game_dir); | ||||
|         game_dir.path = "SYSTEM"; | ||||
|         UISettings::values.game_dirs.append(game_dir); | ||||
|         if (UISettings::values.game_dir_deprecated != ".") { | ||||
|             game_dir.path = UISettings::values.game_dir_deprecated; | ||||
|             game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan; | ||||
|             UISettings::values.game_dirs.append(game_dir); | ||||
|         } | ||||
|     } | ||||
|     UISettings::values.recent_files = qt_config->value("recentFiles").toStringList(); | ||||
|     UISettings::values.language = qt_config->value("language", "").toString(); | ||||
|     qt_config->endGroup(); | ||||
|  | @ -378,8 +404,15 @@ void Config::SaveValues() { | |||
|     qt_config->beginGroup("Paths"); | ||||
|     qt_config->setValue("romsPath", UISettings::values.roms_path); | ||||
|     qt_config->setValue("symbolsPath", UISettings::values.symbols_path); | ||||
|     qt_config->setValue("gameListRootDir", UISettings::values.gamedir); | ||||
|     qt_config->setValue("gameListDeepScan", UISettings::values.gamedir_deepscan); | ||||
|     qt_config->beginWriteArray("gamedirs"); | ||||
|     for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         const auto& game_dir = UISettings::values.game_dirs.at(i); | ||||
|         qt_config->setValue("path", game_dir.path); | ||||
|         qt_config->setValue("deep_scan", game_dir.deep_scan); | ||||
|         qt_config->setValue("expanded", game_dir.expanded); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     qt_config->setValue("recentFiles", UISettings::values.recent_files); | ||||
|     qt_config->setValue("language", UISettings::values.language); | ||||
|     qt_config->endGroup(); | ||||
|  |  | |||
|  | @ -44,7 +44,6 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) | |||
| ConfigureGeneral::~ConfigureGeneral() {} | ||||
| 
 | ||||
| void ConfigureGeneral::setConfiguration() { | ||||
|     ui->toggle_deepscan->setChecked(UISettings::values.gamedir_deepscan); | ||||
|     ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); | ||||
|     ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit); | ||||
| 
 | ||||
|  | @ -60,7 +59,6 @@ void ConfigureGeneral::setConfiguration() { | |||
| } | ||||
| 
 | ||||
| void ConfigureGeneral::applyConfiguration() { | ||||
|     UISettings::values.gamedir_deepscan = ui->toggle_deepscan->isChecked(); | ||||
|     UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); | ||||
|     UISettings::values.theme = | ||||
|         ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString(); | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>345</width> | ||||
|     <height>493</height> | ||||
|     <height>504</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|  | @ -31,13 +31,6 @@ | |||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item> | ||||
|            <widget class="QCheckBox" name="toggle_deepscan"> | ||||
|             <property name="text"> | ||||
|              <string>Search sub-directories for games</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item> | ||||
|            <layout class="QHBoxLayout" name="horizontalLayout_2"> | ||||
|             <item> | ||||
|  |  | |||
|  | @ -66,6 +66,7 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e | |||
|         case Qt::Key_Enter: { | ||||
|             if (gamelist->search_field->visible == 1) { | ||||
|                 QString file_path = gamelist->getLastFilterResultItem(); | ||||
| 
 | ||||
|                 // To avoid loading error dialog loops while confirming them using enter
 | ||||
|                 // Also users usually want to run a diffrent game after closing one
 | ||||
|                 gamelist->search_field->edit_filter->setText(""); | ||||
|  | @ -171,6 +172,15 @@ bool GameList::containsAllWords(QString haystack, QString userinput) { | |||
|                        [haystack](QString s) { return haystack.contains(s); }); | ||||
| } | ||||
| 
 | ||||
| // Syncs the expanded state of Game Directories with settings to persist across sessions
 | ||||
| void GameList::onItemExpanded(const QModelIndex& item) { | ||||
|     GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>(); | ||||
|     if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir || | ||||
|         type == GameListItemType::SystemDir) | ||||
|         item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded = | ||||
|             tree_view->isExpanded(item); | ||||
| } | ||||
| 
 | ||||
| // Event in order to filter the gamelist after editing the searchfield
 | ||||
| void GameList::onTextChanged(const QString& newText) { | ||||
|     int folderCount = tree_view->model()->rowCount(); | ||||
|  | @ -226,6 +236,31 @@ void GameList::onTextChanged(const QString& newText) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameList::onUpdateThemedIcons() { | ||||
|     for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) { | ||||
|         QStandardItem* child = item_model->invisibleRootItem()->child(i); | ||||
| 
 | ||||
|         switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) { | ||||
|         case GameListItemType::InstalledDir: | ||||
|             child->setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole); | ||||
|             break; | ||||
|         case GameListItemType::SystemDir: | ||||
|             child->setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole); | ||||
|             break; | ||||
|         case GameListItemType::CustomDir: { | ||||
|             const UISettings::GameDir* game_dir = | ||||
|                 child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||||
|             QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder"; | ||||
|             child->setData(QIcon::fromTheme(icon_name).pixmap(48), Qt::DecorationRole); | ||||
|             break; | ||||
|         } | ||||
|         case GameListItemType::AddDir: | ||||
|             child->setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameList::onFilterCloseClicked() { | ||||
|     main_window->filterBarSetChecked(false); | ||||
| } | ||||
|  | @ -257,12 +292,16 @@ GameList::GameList(GMainWindow* parent) : QWidget{parent} { | |||
|     item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, "Region"); | ||||
|     item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type"); | ||||
|     item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); | ||||
|     item_model->setSortRole(GameListItemPath::TitleRole); | ||||
| 
 | ||||
|     connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons); | ||||
|     connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); | ||||
|     connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); | ||||
|     connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded); | ||||
|     connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded); | ||||
| 
 | ||||
|     // We must register all custom types with the Qt Automoc system so that we are able to use it
 | ||||
|     // with signals/slots. In this case, QList falls under the umbrells of custom types.
 | ||||
|     // We must register all custom types with the Qt Automoc system so that we are able to use
 | ||||
|     // it with signals/slots. In this case, QList falls under the umbrells of custom types.
 | ||||
|     qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); | ||||
| 
 | ||||
|     layout->setContentsMargins(0, 0, 0, 0); | ||||
|  | @ -290,27 +329,57 @@ void GameList::clearFilter() { | |||
|     search_field->clear(); | ||||
| } | ||||
| 
 | ||||
| void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { | ||||
| void GameList::AddDirEntry(GameListDir* entry_items) { | ||||
|     item_model->invisibleRootItem()->appendRow(entry_items); | ||||
|     tree_view->setExpanded( | ||||
|         entry_items->index(), | ||||
|         entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded); | ||||
| } | ||||
| 
 | ||||
| void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) { | ||||
|     parent->appendRow(entry_items); | ||||
| } | ||||
| 
 | ||||
| void GameList::ValidateEntry(const QModelIndex& item) { | ||||
|     // We don't care about the individual QStandardItem that was selected, but its row.
 | ||||
|     int row = item_model->itemFromIndex(item)->row(); | ||||
|     QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); | ||||
|     QString file_path = child_file->data(GameListItemPath::FullPathRole).toString(); | ||||
|     auto selected = item.sibling(item.row(), 0); | ||||
| 
 | ||||
|     if (file_path.isEmpty()) | ||||
|         return; | ||||
|     std::string std_file_path(file_path.toStdString()); | ||||
|     if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path)) | ||||
|         return; | ||||
|     // Users usually want to run a diffrent game after closing one
 | ||||
|     search_field->clear(); | ||||
|     emit GameChosen(file_path); | ||||
|     switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||||
|     case GameListItemType::Game: { | ||||
|         QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); | ||||
|         if (file_path.isEmpty()) | ||||
|             return; | ||||
|         QFileInfo file_info(file_path); | ||||
|         if (!file_info.exists() || file_info.isDir()) | ||||
|             return; | ||||
|         // Users usually want to run a different game after closing one
 | ||||
|         search_field->clear(); | ||||
|         emit GameChosen(file_path); | ||||
|         break; | ||||
|     } | ||||
|     case GameListItemType::AddDir: | ||||
|         emit AddDirectory(); | ||||
|         break; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool GameList::isEmpty() { | ||||
|     for (int i = 0; i < item_model->rowCount(); i++) { | ||||
|         const QStandardItem* child = item_model->invisibleRootItem()->child(i); | ||||
|         GameListItemType type = static_cast<GameListItemType>(child->type()); | ||||
|         if (!child->hasChildren() && | ||||
|             (type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) { | ||||
|             item_model->invisibleRootItem()->removeRow(child->row()); | ||||
|             i--; | ||||
|         }; | ||||
|     } | ||||
|     return !item_model->invisibleRootItem()->hasChildren(); | ||||
| } | ||||
| 
 | ||||
| void GameList::DonePopulating(QStringList watch_list) { | ||||
|     emit ShowList(!isEmpty()); | ||||
| 
 | ||||
|     item_model->invisibleRootItem()->appendRow(new GameListAddDir()); | ||||
| 
 | ||||
|     // Clear out the old directories to watch for changes and add the new ones
 | ||||
|     auto watch_dirs = watcher->directories(); | ||||
|     if (!watch_dirs.isEmpty()) { | ||||
|  | @ -346,12 +415,25 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
|     if (!item.isValid()) | ||||
|         return; | ||||
| 
 | ||||
|     int row = item_model->itemFromIndex(item)->row(); | ||||
|     QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); | ||||
|     u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong(); | ||||
| 
 | ||||
|     auto selected = item.sibling(item.row(), 0); | ||||
|     QMenu context_menu; | ||||
|     switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||||
|     case GameListItemType::Game: | ||||
|         AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong()); | ||||
|         break; | ||||
|     case GameListItemType::CustomDir: | ||||
|         AddPermDirPopup(context_menu, selected); | ||||
|         AddCustomDirPopup(context_menu, selected); | ||||
|         break; | ||||
|     case GameListItemType::InstalledDir: | ||||
|     case GameListItemType::SystemDir: | ||||
|         AddPermDirPopup(context_menu, selected); | ||||
|         break; | ||||
|     } | ||||
|     context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | ||||
| } | ||||
| 
 | ||||
| void GameList::AddGamePopup(QMenu& context_menu, u64 program_id) { | ||||
|     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); | ||||
|     QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); | ||||
|     QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); | ||||
|  | @ -370,16 +452,81 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
|         }); | ||||
|     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); | ||||
| 
 | ||||
|     connect(open_save_location, &QAction::triggered, | ||||
|             [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); }); | ||||
|     connect(open_application_location, &QAction::triggered, | ||||
|             [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); }); | ||||
|     connect(open_update_location, &QAction::triggered, | ||||
|             [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); }); | ||||
|     connect(navigate_to_gamedb_entry, &QAction::triggered, | ||||
|             [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); | ||||
|     connect(open_save_location, &QAction::triggered, [this, program_id] { | ||||
|         emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); | ||||
|     }); | ||||
|     connect(open_application_location, &QAction::triggered, [this, program_id] { | ||||
|         emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); | ||||
|     }); | ||||
|     connect(open_update_location, &QAction::triggered, [this, program_id] { | ||||
|         emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); | ||||
|     }); | ||||
|     connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { | ||||
|         emit NavigateToGamedbEntryRequested(program_id, compatibility_list); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | ||||
| void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { | ||||
|     UISettings::GameDir& game_dir = | ||||
|         *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||||
| 
 | ||||
|     QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); | ||||
|     QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); | ||||
| 
 | ||||
|     deep_scan->setCheckable(true); | ||||
|     deep_scan->setChecked(game_dir.deep_scan); | ||||
| 
 | ||||
|     connect(deep_scan, &QAction::triggered, [this, &game_dir] { | ||||
|         game_dir.deep_scan = !game_dir.deep_scan; | ||||
|         PopulateAsync(UISettings::values.game_dirs); | ||||
|     }); | ||||
|     connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { | ||||
|         UISettings::values.game_dirs.removeOne(game_dir); | ||||
|         item_model->invisibleRootItem()->removeRow(selected.row()); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { | ||||
|     UISettings::GameDir& game_dir = | ||||
|         *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||||
| 
 | ||||
|     QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up")); | ||||
|     QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down ")); | ||||
|     QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location")); | ||||
| 
 | ||||
|     int row = selected.row(); | ||||
| 
 | ||||
|     move_up->setEnabled(row > 0); | ||||
|     move_down->setEnabled(row < item_model->rowCount() - 2); | ||||
| 
 | ||||
|     connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] { | ||||
|         // find the indices of the items in settings and swap them
 | ||||
|         UISettings::values.game_dirs.swap( | ||||
|             UISettings::values.game_dirs.indexOf(game_dir), | ||||
|             UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() - 1, 0) | ||||
|                                                       .data(GameListDir::GameDirRole) | ||||
|                                                       .value<UISettings::GameDir*>())); | ||||
|         // move the treeview items
 | ||||
|         QList<QStandardItem*> item = item_model->takeRow(row); | ||||
|         item_model->invisibleRootItem()->insertRow(row - 1, item); | ||||
|         tree_view->setExpanded(selected, game_dir.expanded); | ||||
|     }); | ||||
| 
 | ||||
|     connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] { | ||||
|         // find the indices of the items in settings and swap them
 | ||||
|         UISettings::values.game_dirs.swap( | ||||
|             UISettings::values.game_dirs.indexOf(game_dir), | ||||
|             UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() + 1, 0) | ||||
|                                                       .data(GameListDir::GameDirRole) | ||||
|                                                       .value<UISettings::GameDir*>())); | ||||
|         // move the treeview items
 | ||||
|         QList<QStandardItem*> item = item_model->takeRow(row); | ||||
|         item_model->invisibleRootItem()->insertRow(row + 1, item); | ||||
|         tree_view->setExpanded(selected, game_dir.expanded); | ||||
|     }); | ||||
| 
 | ||||
|     connect(open_directory_location, &QAction::triggered, | ||||
|             [this, game_dir] { emit OpenDirectory(game_dir.path); }); | ||||
| } | ||||
| 
 | ||||
| void GameList::LoadCompatibilityList() { | ||||
|  | @ -428,14 +575,7 @@ QStandardItemModel* GameList::GetModel() const { | |||
|     return item_model; | ||||
| } | ||||
| 
 | ||||
| void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | ||||
|     if (!FileUtil::Exists(dir_path.toStdString()) || | ||||
|         !FileUtil::IsDirectory(dir_path.toStdString())) { | ||||
|         NGLOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString()); | ||||
|         search_field->setFilterResult(0, 0); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
| void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) { | ||||
|     tree_view->setEnabled(false); | ||||
|     // Delete any rows that might already exist if we're repopulating
 | ||||
|     item_model->removeRows(0, item_model->rowCount()); | ||||
|  | @ -443,13 +583,15 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | |||
| 
 | ||||
|     emit ShouldCancelWorker(); | ||||
| 
 | ||||
|     GameListWorker* worker = new GameListWorker(dir_path, deep_scan, compatibility_list); | ||||
|     GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list); | ||||
| 
 | ||||
|     connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); | ||||
|     connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, | ||||
|             Qt::QueuedConnection); | ||||
|     connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, | ||||
|             Qt::QueuedConnection); | ||||
|     // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel
 | ||||
|     // without delay.
 | ||||
|     // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
 | ||||
|     // cancel without delay.
 | ||||
|     connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, | ||||
|             Qt::DirectConnection); | ||||
| 
 | ||||
|  | @ -481,15 +623,17 @@ static bool HasSupportedFileExtension(const std::string& file_name) { | |||
| } | ||||
| 
 | ||||
| void GameList::RefreshGameDirectory() { | ||||
|     if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) { | ||||
|     if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { | ||||
|         NGLOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); | ||||
|         PopulateAsync(UISettings::values.gamedirs); | ||||
|         PopulateAsync(UISettings::values.game_dirs); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | ||||
|     const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory, | ||||
|                                             const std::string& virtual_name) -> bool { | ||||
| void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, | ||||
|                                              GameListDir* parent_dir) { | ||||
|     const auto callback = [this, recursion, parent_dir](unsigned* num_entries_out, | ||||
|                                                         const std::string& directory, | ||||
|                                                         const std::string& virtual_name) -> bool { | ||||
|         std::string physical_name = directory + DIR_SEP + virtual_name; | ||||
| 
 | ||||
|         if (stop_processing) | ||||
|  | @ -539,17 +683,20 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
|             if (it != compatibility_list.end()) | ||||
|                 compatibility = it->second.first; | ||||
| 
 | ||||
|             emit EntryReady({ | ||||
|                 new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), | ||||
|                 new GameListItemCompat(compatibility), | ||||
|                 new GameListItemRegion(smdh), | ||||
|                 new GameListItem( | ||||
|                     QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||||
|                 new GameListItemSize(FileUtil::GetSize(physical_name)), | ||||
|             }); | ||||
|             emit EntryReady( | ||||
|                 { | ||||
|                     new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), | ||||
|                     new GameListItemCompat(compatibility), | ||||
|                     new GameListItemRegion(smdh), | ||||
|                     new GameListItem( | ||||
|                         QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||||
|                     new GameListItemSize(FileUtil::GetSize(physical_name)), | ||||
|                 }, | ||||
|                 parent_dir); | ||||
| 
 | ||||
|         } else if (is_dir && recursion > 0) { | ||||
|             watch_list.append(QString::fromStdString(physical_name)); | ||||
|             AddFstEntriesToGameList(physical_name, recursion - 1); | ||||
|             AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|  | @ -560,27 +707,33 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
| 
 | ||||
| void GameListWorker::run() { | ||||
|     stop_processing = false; | ||||
|     watch_list.append(dir_path); | ||||
|     watch_list.append(QString::fromStdString( | ||||
|         std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) + | ||||
|         "Nintendo " | ||||
|         "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000")); | ||||
|     watch_list.append(QString::fromStdString( | ||||
|         std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) + | ||||
|         "Nintendo " | ||||
|         "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004000e")); | ||||
|     watch_list.append( | ||||
|         QString::fromStdString(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) + | ||||
|                                "00000000000000000000000000000000/title/00040010")); | ||||
|     AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); | ||||
|     AddFstEntriesToGameList( | ||||
|         std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) + | ||||
|             "Nintendo " | ||||
|             "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000", | ||||
|         2); | ||||
|     AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) + | ||||
|                                 "00000000000000000000000000000000/title/00040010", | ||||
|                             2); | ||||
|     for (UISettings::GameDir& game_dir : game_dirs) { | ||||
|         if (game_dir.path == "INSTALLED") { | ||||
|             QString path = QString(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) + | ||||
|                            "Nintendo " | ||||
|                            "3DS/00000000000000000000000000000000/" | ||||
|                            "00000000000000000000000000000000/title/00040000"; | ||||
|             watch_list.append(path); | ||||
|             GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); | ||||
|             emit DirEntryReady({game_list_dir}); | ||||
|             AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); | ||||
|         } else if (game_dir.path == "SYSTEM") { | ||||
|             QString path = QString(FileUtil::GetUserPath(D_NAND_IDX).c_str()) + | ||||
|                            "00000000000000000000000000000000/title/00040010"; | ||||
|             watch_list.append(path); | ||||
|             GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); | ||||
|             emit DirEntryReady({game_list_dir}); | ||||
|             AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) + | ||||
|                                         "00000000000000000000000000000000/title/00040010", | ||||
|                                     2, game_list_dir); | ||||
|         } else { | ||||
|             watch_list.append(game_dir.path); | ||||
|             GameListDir* game_list_dir = new GameListDir(game_dir); | ||||
|             emit DirEntryReady({game_list_dir}); | ||||
|             AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0, | ||||
|                                     game_list_dir); | ||||
|         } | ||||
|     }; | ||||
|     emit Finished(watch_list); | ||||
| } | ||||
| 
 | ||||
|  | @ -588,3 +741,37 @@ void GameListWorker::Cancel() { | |||
|     this->disconnect(); | ||||
|     stop_processing = true; | ||||
| } | ||||
| 
 | ||||
| GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { | ||||
|     this->main_window = parent; | ||||
| 
 | ||||
|     connect(main_window, &GMainWindow::UpdateThemedIcons, this, | ||||
|             &GameListPlaceholder::onUpdateThemedIcons); | ||||
| 
 | ||||
|     layout = new QVBoxLayout; | ||||
|     image = new QLabel; | ||||
|     text = new QLabel; | ||||
|     layout->setAlignment(Qt::AlignCenter); | ||||
|     image->setPixmap(QIcon::fromTheme("plus_folder").pixmap(200)); | ||||
| 
 | ||||
|     text->setText(tr("Double-click to add a new folder to the game list ")); | ||||
|     QFont font = text->font(); | ||||
|     font.setPointSize(20); | ||||
|     text->setFont(font); | ||||
|     text->setAlignment(Qt::AlignHCenter); | ||||
|     image->setAlignment(Qt::AlignHCenter); | ||||
| 
 | ||||
|     layout->addWidget(image); | ||||
|     layout->addWidget(text); | ||||
|     setLayout(layout); | ||||
| } | ||||
| 
 | ||||
| GameListPlaceholder::~GameListPlaceholder() = default; | ||||
| 
 | ||||
| void GameListPlaceholder::onUpdateThemedIcons() { | ||||
|     image->setPixmap(QIcon::fromTheme("plus_folder").pixmap(200)); | ||||
| } | ||||
| 
 | ||||
| void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { | ||||
|     emit GameListPlaceholder::AddDirectory(); | ||||
| } | ||||
|  |  | |||
|  | @ -8,13 +8,17 @@ | |||
| #include <QString> | ||||
| #include <QWidget> | ||||
| #include "common/common_types.h" | ||||
| #include "ui_settings.h" | ||||
| 
 | ||||
| class GameListWorker; | ||||
| class GameListDir; | ||||
| class GMainWindow; | ||||
| class QFileSystemWatcher; | ||||
| class QHBoxLayout; | ||||
| class QLabel; | ||||
| class QLineEdit; | ||||
| template <typename> | ||||
| class QList; | ||||
| class QModelIndex; | ||||
| class QStandardItem; | ||||
| class QStandardItemModel; | ||||
|  | @ -39,12 +43,14 @@ public: | |||
| 
 | ||||
|     class SearchField : public QWidget { | ||||
|     public: | ||||
|         int visible; | ||||
|         int total; | ||||
|         explicit SearchField(GameList* parent = nullptr); | ||||
| 
 | ||||
|         void setFilterResult(int visible, int total); | ||||
|         void clear(); | ||||
|         void setFocus(); | ||||
|         explicit SearchField(GameList* parent = nullptr); | ||||
| 
 | ||||
|         int visible; | ||||
|         int total; | ||||
| 
 | ||||
|     private: | ||||
|         class KeyReleaseEater : public QObject { | ||||
|  | @ -73,9 +79,10 @@ public: | |||
|     void clearFilter(); | ||||
|     void setFilterFocus(); | ||||
|     void setFilterVisible(bool visibility); | ||||
|     bool isEmpty(); | ||||
| 
 | ||||
|     void LoadCompatibilityList(); | ||||
|     void PopulateAsync(const QString& dir_path, bool deep_scan); | ||||
|     void PopulateAsync(QList<UISettings::GameDir>& game_dirs); | ||||
| 
 | ||||
|     void SaveInterfaceLayout(); | ||||
|     void LoadInterfaceLayout(); | ||||
|  | @ -91,20 +98,30 @@ signals: | |||
|     void NavigateToGamedbEntryRequested( | ||||
|         u64 program_id, | ||||
|         std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list); | ||||
|     void OpenDirectory(QString directory); | ||||
|     void AddDirectory(); | ||||
|     void ShowList(bool show); | ||||
| 
 | ||||
| private slots: | ||||
|     void onItemExpanded(const QModelIndex& item); | ||||
|     void onTextChanged(const QString& newText); | ||||
|     void onFilterCloseClicked(); | ||||
|     void onUpdateThemedIcons(); | ||||
| 
 | ||||
| private: | ||||
|     void AddEntry(const QList<QStandardItem*>& entry_items); | ||||
|     void AddDirEntry(GameListDir* entry_items); | ||||
|     void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent); | ||||
|     void ValidateEntry(const QModelIndex& item); | ||||
|     void DonePopulating(QStringList watch_list); | ||||
| 
 | ||||
|     void PopupContextMenu(const QPoint& menu_location); | ||||
|     void RefreshGameDirectory(); | ||||
|     bool containsAllWords(QString haystack, QString userinput); | ||||
| 
 | ||||
|     void PopupContextMenu(const QPoint& menu_location); | ||||
|     void AddGamePopup(QMenu& context_menu, u64 program_id); | ||||
|     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); | ||||
|     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); | ||||
| 
 | ||||
|     SearchField* search_field; | ||||
|     GMainWindow* main_window = nullptr; | ||||
|     QVBoxLayout* layout = nullptr; | ||||
|  | @ -116,3 +133,25 @@ private: | |||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(GameListOpenTarget); | ||||
| 
 | ||||
| class GameListPlaceholder : public QWidget { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     explicit GameListPlaceholder(GMainWindow* parent = nullptr); | ||||
|     ~GameListPlaceholder(); | ||||
| 
 | ||||
| signals: | ||||
|     void AddDirectory(); | ||||
| 
 | ||||
| private slots: | ||||
|     void onUpdateThemedIcons(); | ||||
| 
 | ||||
| protected: | ||||
|     void mouseDoubleClickEvent(QMouseEvent* event) override; | ||||
| 
 | ||||
| private: | ||||
|     GMainWindow* main_window = nullptr; | ||||
|     QVBoxLayout* layout = nullptr; | ||||
|     QLabel* image = nullptr; | ||||
|     QLabel* text = nullptr; | ||||
| }; | ||||
|  |  | |||
|  | @ -8,17 +8,29 @@ | |||
| #include <map> | ||||
| #include <unordered_map> | ||||
| #include <QCoreApplication> | ||||
| #include <QFileInfo> | ||||
| #include <QImage> | ||||
| #include <QObject> | ||||
| #include <QPainter> | ||||
| #include <QRunnable> | ||||
| #include <QStandardItem> | ||||
| #include <QString> | ||||
| #include "citra_qt/ui_settings.h" | ||||
| #include "citra_qt/util/util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/string_util.h" | ||||
| #include "core/loader/smdh.h" | ||||
| 
 | ||||
| enum class GameListItemType { | ||||
|     Game = QStandardItem::UserType + 1, | ||||
|     CustomDir = QStandardItem::UserType + 2, | ||||
|     InstalledDir = QStandardItem::UserType + 3, | ||||
|     SystemDir = QStandardItem::UserType + 4, | ||||
|     AddDir = QStandardItem::UserType + 5 | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(GameListItemType); | ||||
| 
 | ||||
| /**
 | ||||
|  * Gets the game icon from SMDH data. | ||||
|  * @param smdh SMDH data | ||||
|  | @ -125,8 +137,13 @@ const static inline std::map<QString, CompatStatus> status_data = { | |||
| 
 | ||||
| class GameListItem : public QStandardItem { | ||||
| public: | ||||
|     // used to access type from item index
 | ||||
|     static const int TypeRole = Qt::UserRole + 1; | ||||
|     static const int SortRole = Qt::UserRole + 2; | ||||
|     GameListItem() : QStandardItem() {} | ||||
|     GameListItem(const QString& string) : QStandardItem(string) {} | ||||
|     GameListItem(const QString& string) : QStandardItem(string) { | ||||
|         setData(string, SortRole); | ||||
|     } | ||||
|     virtual ~GameListItem() override {} | ||||
| }; | ||||
| 
 | ||||
|  | @ -138,13 +155,14 @@ public: | |||
|  */ | ||||
| class GameListItemPath : public GameListItem { | ||||
| public: | ||||
|     static const int FullPathRole = Qt::UserRole + 1; | ||||
|     static const int TitleRole = Qt::UserRole + 2; | ||||
|     static const int ProgramIdRole = Qt::UserRole + 3; | ||||
|     static const int TitleRole = SortRole; | ||||
|     static const int FullPathRole = SortRole + 1; | ||||
|     static const int ProgramIdRole = SortRole + 2; | ||||
| 
 | ||||
|     GameListItemPath() : GameListItem() {} | ||||
|     GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id) | ||||
|         : GameListItem() { | ||||
|         setData(type(), TypeRole); | ||||
|         setData(game_path, FullPathRole); | ||||
|         setData(qulonglong(program_id), ProgramIdRole); | ||||
| 
 | ||||
|  | @ -165,6 +183,10 @@ public: | |||
|                 TitleRole); | ||||
|     } | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(GameListItemType::Game); | ||||
|     } | ||||
| 
 | ||||
|     QVariant data(int role) const override { | ||||
|         if (role == Qt::DisplayRole) { | ||||
|             std::string filename; | ||||
|  | @ -180,9 +202,12 @@ public: | |||
| 
 | ||||
| class GameListItemCompat : public GameListItem { | ||||
| public: | ||||
|     static const int CompatNumberRole = Qt::UserRole + 1; | ||||
|     static const int CompatNumberRole = SortRole; | ||||
| 
 | ||||
|     GameListItemCompat() = default; | ||||
|     explicit GameListItemCompat(const QString compatiblity) { | ||||
|         setData(type(), TypeRole); | ||||
| 
 | ||||
|         auto iterator = status_data.find(compatiblity); | ||||
|         if (iterator == status_data.end()) { | ||||
|             NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString()); | ||||
|  | @ -195,6 +220,10 @@ public: | |||
|         setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); | ||||
|     } | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(GameListItemType::Game); | ||||
|     } | ||||
| 
 | ||||
|     bool operator<(const QStandardItem& other) const override { | ||||
|         return data(CompatNumberRole) < other.data(CompatNumberRole); | ||||
|     } | ||||
|  | @ -204,6 +233,8 @@ class GameListItemRegion : public GameListItem { | |||
| public: | ||||
|     GameListItemRegion() = default; | ||||
|     explicit GameListItemRegion(const std::vector<u8>& smdh_data) { | ||||
|         setData(type(), TypeRole); | ||||
| 
 | ||||
|         if (!Loader::IsValidSMDH(smdh_data)) { | ||||
|             setText(QObject::tr("Invalid region")); | ||||
|             return; | ||||
|  | @ -213,6 +244,11 @@ public: | |||
|         memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); | ||||
| 
 | ||||
|         setText(GetRegionFromSMDH(smdh)); | ||||
|         setData(GetRegionFromSMDH(smdh), SortRole); | ||||
|     } | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(GameListItemType::Game); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
|  | @ -223,10 +259,11 @@ public: | |||
|  */ | ||||
| class GameListItemSize : public GameListItem { | ||||
| public: | ||||
|     static const int SizeRole = Qt::UserRole + 1; | ||||
|     static const int SizeRole = SortRole; | ||||
| 
 | ||||
|     GameListItemSize() : GameListItem() {} | ||||
|     GameListItemSize(const qulonglong size_bytes) : GameListItem() { | ||||
|         setData(type(), TypeRole); | ||||
|         setData(size_bytes, SizeRole); | ||||
|     } | ||||
| 
 | ||||
|  | @ -242,6 +279,10 @@ public: | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(GameListItemType::Game); | ||||
|     } | ||||
| 
 | ||||
|     /**
 | ||||
|      * This operator is, in practice, only used by the TreeView sorting systems. | ||||
|      * Override it so that it will correctly sort by numerical value instead of by string | ||||
|  | @ -252,6 +293,55 @@ public: | |||
|     } | ||||
| }; | ||||
| 
 | ||||
| class GameListDir : public GameListItem { | ||||
| public: | ||||
|     static const int GameDirRole = Qt::UserRole + 2; | ||||
| 
 | ||||
|     explicit GameListDir(UISettings::GameDir& directory, | ||||
|                          GameListItemType dir_type = GameListItemType::CustomDir) | ||||
|         : dir_type{dir_type} { | ||||
|         setData(type(), TypeRole); | ||||
| 
 | ||||
|         UISettings::GameDir* game_dir = &directory; | ||||
|         setData(QVariant::fromValue(game_dir), GameDirRole); | ||||
|         switch (dir_type) { | ||||
|         case GameListItemType::InstalledDir: | ||||
|             setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole); | ||||
|             setData("Installed Titles", Qt::DisplayRole); | ||||
|             break; | ||||
|         case GameListItemType::SystemDir: | ||||
|             setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole); | ||||
|             setData("System Titles", Qt::DisplayRole); | ||||
|             break; | ||||
|         case GameListItemType::CustomDir: | ||||
|             QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder"; | ||||
|             setData(QIcon::fromTheme(icon_name).pixmap(48), Qt::DecorationRole); | ||||
|             setData(game_dir->path, Qt::DisplayRole); | ||||
|             break; | ||||
|         }; | ||||
|     }; | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(dir_type); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     GameListItemType dir_type; | ||||
| }; | ||||
| 
 | ||||
| class GameListAddDir : public GameListItem { | ||||
| public: | ||||
|     explicit GameListAddDir() { | ||||
|         setData(type(), TypeRole); | ||||
|         setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole); | ||||
|         setData("Add New Game Directory", Qt::DisplayRole); | ||||
|     } | ||||
| 
 | ||||
|     int type() const override { | ||||
|         return static_cast<int>(GameListItemType::AddDir); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * Asynchronous worker object for populating the game list. | ||||
|  * Communicates with other threads through Qt's signal/slot system. | ||||
|  | @ -260,11 +350,10 @@ class GameListWorker : public QObject, public QRunnable { | |||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     GameListWorker( | ||||
|         QString dir_path, bool deep_scan, | ||||
|     explicit GameListWorker( | ||||
|         QList<UISettings::GameDir>& game_dirs, | ||||
|         const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list) | ||||
|         : QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan), | ||||
|           compatibility_list(compatibility_list) {} | ||||
|         : QObject(), QRunnable(), game_dirs(game_dirs), compatibility_list(compatibility_list) {} | ||||
| 
 | ||||
| public slots: | ||||
|     /// Starts the processing of directory tree information.
 | ||||
|  | @ -276,22 +365,24 @@ signals: | |||
|     /**
 | ||||
|      * The `EntryReady` signal is emitted once an entry has been prepared and is ready | ||||
|      * to be added to the game list. | ||||
|      * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. | ||||
|      * @param entry_items a list with `QStandardItem`s that make up the columns of the new | ||||
|      * entry. | ||||
|      */ | ||||
|     void EntryReady(QList<QStandardItem*> entry_items); | ||||
|     void DirEntryReady(GameListDir* entry_items); | ||||
|     void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir); | ||||
| 
 | ||||
|     /**
 | ||||
|      * After the worker has traversed the game directory looking for entries, this signal is emmited | ||||
|      * with a list of folders that should be watched for changes as well. | ||||
|      * After the worker has traversed the game directory looking for entries, this signal is | ||||
|      * emitted with a list of folders that should be watched for changes as well. | ||||
|      */ | ||||
|     void Finished(QStringList watch_list); | ||||
| 
 | ||||
| private: | ||||
|     QStringList watch_list; | ||||
|     QString dir_path; | ||||
|     bool deep_scan; | ||||
|     const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list; | ||||
|     QList<UISettings::GameDir>& game_dirs; | ||||
|     std::atomic_bool stop_processing; | ||||
| 
 | ||||
|     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); | ||||
|     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, | ||||
|                                  GameListDir* parent_dir); | ||||
| }; | ||||
|  |  | |||
|  | @ -143,7 +143,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { | |||
|     show(); | ||||
| 
 | ||||
|     game_list->LoadCompatibilityList(); | ||||
|     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||||
|     game_list->PopulateAsync(UISettings::values.game_dirs); | ||||
| 
 | ||||
|     // Show one-time "callout" messages to the user
 | ||||
|     ShowCallouts(); | ||||
|  | @ -177,6 +177,10 @@ void GMainWindow::InitializeWidgets() { | |||
|     game_list = new GameList(this); | ||||
|     ui.horizontalLayout->addWidget(game_list); | ||||
| 
 | ||||
|     game_list_placeholder = new GameListPlaceholder(this); | ||||
|     ui.horizontalLayout->addWidget(game_list_placeholder); | ||||
|     game_list_placeholder->setVisible(false); | ||||
| 
 | ||||
|     multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room, | ||||
|                                              ui.action_Show_Room); | ||||
|     multiplayer_state->setVisible(false); | ||||
|  | @ -399,9 +403,14 @@ void GMainWindow::RestoreUIState() { | |||
| 
 | ||||
| void GMainWindow::ConnectWidgetEvents() { | ||||
|     connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); | ||||
|     connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); | ||||
|     connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); | ||||
|     connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, | ||||
|             &GMainWindow::OnGameListNavigateToGamedbEntry); | ||||
|     connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); | ||||
|     connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, | ||||
|             &GMainWindow::OnGameListAddDirectory); | ||||
|     connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); | ||||
| 
 | ||||
|     connect(this, &GMainWindow::EmulationStarting, render_window, | ||||
|             &GRenderWindow::OnEmulationStarting); | ||||
|  | @ -419,8 +428,6 @@ void GMainWindow::ConnectMenuEvents() { | |||
|     // File
 | ||||
|     connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile); | ||||
|     connect(ui.action_Install_CIA, &QAction::triggered, this, &GMainWindow::OnMenuInstallCIA); | ||||
|     connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, | ||||
|             &GMainWindow::OnMenuSelectGameListRoot); | ||||
|     connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); | ||||
| 
 | ||||
|     // Emulation
 | ||||
|  | @ -672,6 +679,7 @@ void GMainWindow::BootGame(const QString& filename) { | |||
|     registersWidget->OnDebugModeEntered(); | ||||
|     if (ui.action_Single_Window_Mode->isChecked()) { | ||||
|         game_list->hide(); | ||||
|         game_list_placeholder->hide(); | ||||
|     } | ||||
|     status_bar_update_timer.start(2000); | ||||
| 
 | ||||
|  | @ -713,7 +721,10 @@ void GMainWindow::ShutdownGame() { | |||
|     ui.action_Stop->setEnabled(false); | ||||
|     ui.action_Report_Compatibility->setEnabled(false); | ||||
|     render_window->hide(); | ||||
|     game_list->show(); | ||||
|     if (game_list->isEmpty()) | ||||
|         game_list_placeholder->show(); | ||||
|     else | ||||
|         game_list->show(); | ||||
|     game_list->setFilterFocus(); | ||||
| 
 | ||||
|     // Disable status bar updates
 | ||||
|  | @ -828,6 +839,48 @@ void GMainWindow::OnGameListNavigateToGamedbEntry( | |||
|     QDesktopServices::openUrl(QUrl("https://citra-emu.org/game/" + directory)); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnGameListOpenDirectory(QString directory) { | ||||
|     QString path; | ||||
|     if (directory == "INSTALLED") { | ||||
|         path = | ||||
|             QString::fromStdString(FileUtil::GetUserPath(D_SDMC_IDX).c_str() + | ||||
|                                    std::string("Nintendo " | ||||
|                                                "3DS/00000000000000000000000000000000/" | ||||
|                                                "00000000000000000000000000000000/title/00040000")); | ||||
|     } else if (directory == "SYSTEM") { | ||||
|         path = | ||||
|             QString::fromStdString(FileUtil::GetUserPath(D_NAND_IDX).c_str() + | ||||
|                                    std::string("00000000000000000000000000000000/title/00040010")); | ||||
|     } else { | ||||
|         path = directory; | ||||
|     } | ||||
|     if (!QFileInfo::exists(path)) { | ||||
|         QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); | ||||
|         return; | ||||
|     } | ||||
|     QDesktopServices::openUrl(QUrl::fromLocalFile(path)); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnGameListAddDirectory() { | ||||
|     QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||||
|     if (dir_path.isEmpty()) | ||||
|         return; | ||||
|     UISettings::GameDir game_dir{dir_path, false, true}; | ||||
|     if (!UISettings::values.game_dirs.contains(game_dir)) { | ||||
|         UISettings::values.game_dirs.append(game_dir); | ||||
|         game_list->PopulateAsync(UISettings::values.game_dirs); | ||||
|     } else { | ||||
|         NGLOG_WARNING(Frontend, "Selected directory is already in the game list"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnGameListShowList(bool show) { | ||||
|     if (emulation_running && ui.action_Single_Window_Mode->isChecked()) | ||||
|         return; | ||||
|     game_list->setVisible(show); | ||||
|     game_list_placeholder->setVisible(!show); | ||||
| }; | ||||
| 
 | ||||
| void GMainWindow::OnMenuLoadFile() { | ||||
|     QString extensions; | ||||
|     for (const auto& piece : game_list->supported_file_extensions) | ||||
|  | @ -845,14 +898,6 @@ void GMainWindow::OnMenuLoadFile() { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnMenuSelectGameListRoot() { | ||||
|     QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||||
|     if (!dir_path.isEmpty()) { | ||||
|         UISettings::values.gamedir = dir_path; | ||||
|         game_list->PopulateAsync(dir_path, UISettings::values.gamedir_deepscan); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnMenuInstallCIA() { | ||||
|     QStringList filepaths = QFileDialog::getOpenFileNames( | ||||
|         this, tr("Load Files"), UISettings::values.roms_path, | ||||
|  | @ -1089,6 +1134,7 @@ void GMainWindow::OnConfigure() { | |||
|     if (result == QDialog::Accepted) { | ||||
|         configureDialog.applyConfiguration(); | ||||
|         UpdateUITheme(); | ||||
|         emit UpdateThemedIcons(); | ||||
|         SyncMenuUISettings(); | ||||
|         config->Save(); | ||||
|     } | ||||
|  | @ -1308,7 +1354,6 @@ void GMainWindow::UpdateUITheme() { | |||
|         QIcon::setThemeName(":/icons/default"); | ||||
|     } | ||||
|     QIcon::setThemeSearchPaths(theme_paths); | ||||
|     emit UpdateThemedIcons(); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::LoadTranslation() { | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ class ClickableLabel; | |||
| class EmuThread; | ||||
| class GameList; | ||||
| enum class GameListOpenTarget; | ||||
| class GameListPlaceholder; | ||||
| class GImageInfo; | ||||
| class GPUCommandListWidget; | ||||
| class GPUCommandStreamWidget; | ||||
|  | @ -148,13 +149,14 @@ private slots: | |||
|     void OnGameListNavigateToGamedbEntry( | ||||
|         u64 program_id, | ||||
|         std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list); | ||||
|     void OnGameListOpenDirectory(QString path); | ||||
|     void OnGameListAddDirectory(); | ||||
|     void OnGameListShowList(bool show); | ||||
|     void OnMenuLoadFile(); | ||||
|     void OnMenuInstallCIA(); | ||||
|     void OnUpdateProgress(size_t written, size_t total); | ||||
|     void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath); | ||||
|     void OnCIAInstallFinished(); | ||||
|     /// Called whenever a user selects the "File->Select Game List Root" menu item
 | ||||
|     void OnMenuSelectGameListRoot(); | ||||
|     void OnMenuRecentFile(); | ||||
|     void OnConfigure(); | ||||
|     void OnToggleFilterBar(); | ||||
|  | @ -184,6 +186,8 @@ private: | |||
| 
 | ||||
|     GRenderWindow* render_window; | ||||
| 
 | ||||
|     GameListPlaceholder* game_list_placeholder; | ||||
| 
 | ||||
|     // Status bar elements
 | ||||
|     QProgressBar* progress_bar = nullptr; | ||||
|     QLabel* message_label = nullptr; | ||||
|  |  | |||
|  | @ -60,7 +60,6 @@ | |||
|     <addaction name="action_Load_File"/> | ||||
|     <addaction name="action_Install_CIA"/> | ||||
|     <addaction name="separator"/> | ||||
|     <addaction name="action_Select_Game_List_Root"/> | ||||
|     <addaction name="menu_recent_files"/> | ||||
|     <addaction name="separator"/> | ||||
|     <addaction name="action_Exit"/> | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
| #include <array> | ||||
| #include <vector> | ||||
| #include <QByteArray> | ||||
| #include <QMetaType> | ||||
| #include <QString> | ||||
| #include <QStringList> | ||||
| 
 | ||||
|  | @ -19,6 +20,18 @@ static const std::array<std::pair<QString, QString>, 2> themes = { | |||
|     {std::make_pair(QString("Default"), QString("default")), | ||||
|      std::make_pair(QString("Dark"), QString("qdarkstyle"))}}; | ||||
| 
 | ||||
| struct GameDir { | ||||
|     QString path; | ||||
|     bool deep_scan; | ||||
|     bool expanded; | ||||
|     bool operator==(const GameDir& rhs) const { | ||||
|         return path == rhs.path; | ||||
|     }; | ||||
|     bool operator!=(const GameDir& rhs) const { | ||||
|         return !operator==(rhs); | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| struct Values { | ||||
|     QByteArray geometry; | ||||
|     QByteArray state; | ||||
|  | @ -45,8 +58,9 @@ struct Values { | |||
| 
 | ||||
|     QString roms_path; | ||||
|     QString symbols_path; | ||||
|     QString gamedir; | ||||
|     bool gamedir_deepscan; | ||||
|     QString game_dir_deprecated; | ||||
|     bool game_dir_deprecated_deepscan; | ||||
|     QList<UISettings::GameDir> game_dirs; | ||||
|     QStringList recent_files; | ||||
|     QString language; | ||||
| 
 | ||||
|  | @ -74,3 +88,5 @@ struct Values { | |||
| 
 | ||||
| extern Values values; | ||||
| } // namespace UISettings
 | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(UISettings::GameDir*); | ||||
|  |  | |||