diff options
| author | 2019-05-01 23:21:04 +0200 | |
|---|---|---|
| committer | 2019-09-04 16:47:32 +0200 | |
| commit | 2d8eba5bafd7fe9da00c8a57c605a503c3ece478 (patch) | |
| tree | c3bc64c33ab43f6bd0d7bb6f4ec63b11490a41e8 /src | |
| parent | Add assets and licenses (diff) | |
| download | yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.gz yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.tar.xz yuzu-2d8eba5bafd7fe9da00c8a57c605a503c3ece478.zip | |
yuzu: Add support for multiple game directories
Ported from https://github.com/citra-emu/citra/pull/3617.
Diffstat (limited to 'src')
| -rw-r--r-- | src/yuzu/configuration/config.cpp | 42 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_general.cpp | 5 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_general.ui | 7 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 430 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 44 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 111 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.cpp | 83 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.h | 25 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 85 | ||||
| -rw-r--r-- | src/yuzu/main.h | 8 | ||||
| -rw-r--r-- | src/yuzu/main.ui | 1 | ||||
| -rw-r--r-- | src/yuzu/uisettings.h | 20 |
12 files changed, 666 insertions, 195 deletions
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 0456248ac..f2f116a87 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp | |||
| @@ -517,10 +517,35 @@ void Config::ReadPathValues() { | |||
| 517 | UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString(); | 517 | UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString(); |
| 518 | UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString(); | 518 | UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString(); |
| 519 | UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); | 519 | UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); |
| 520 | UISettings::values.game_directory_path = | 520 | UISettings::values.game_dir_deprecated = |
| 521 | ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); | 521 | ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); |
| 522 | UISettings::values.game_directory_deepscan = | 522 | UISettings::values.game_dir_deprecated_deepscan = |
| 523 | ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); | 523 | ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); |
| 524 | int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs")); | ||
| 525 | for (int i = 0; i < gamedirs_size; ++i) { | ||
| 526 | qt_config->setArrayIndex(i); | ||
| 527 | UISettings::GameDir game_dir; | ||
| 528 | game_dir.path = ReadSetting(QStringLiteral("path")).toString(); | ||
| 529 | game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool(); | ||
| 530 | game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool(); | ||
| 531 | UISettings::values.game_dirs.append(game_dir); | ||
| 532 | } | ||
| 533 | qt_config->endArray(); | ||
| 534 | // create NAND and SD card directories if empty, these are not removable through the UI, | ||
| 535 | // also carries over old game list settings if present | ||
| 536 | if (UISettings::values.game_dirs.isEmpty()) { | ||
| 537 | UISettings::GameDir game_dir; | ||
| 538 | game_dir.path = QStringLiteral("INSTALLED"); | ||
| 539 | game_dir.expanded = true; | ||
| 540 | UISettings::values.game_dirs.append(game_dir); | ||
| 541 | game_dir.path = QStringLiteral("SYSTEM"); | ||
| 542 | UISettings::values.game_dirs.append(game_dir); | ||
| 543 | if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) { | ||
| 544 | game_dir.path = UISettings::values.game_dir_deprecated; | ||
| 545 | game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan; | ||
| 546 | UISettings::values.game_dirs.append(game_dir); | ||
| 547 | } | ||
| 548 | } | ||
| 524 | UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); | 549 | UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); |
| 525 | 550 | ||
| 526 | qt_config->endGroup(); | 551 | qt_config->endGroup(); |
| @@ -899,10 +924,15 @@ void Config::SavePathValues() { | |||
| 899 | WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); | 924 | WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); |
| 900 | WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); | 925 | WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); |
| 901 | WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); | 926 | WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); |
| 902 | WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, | 927 | qt_config->beginWriteArray(QStringLiteral("gamedirs")); |
| 903 | QStringLiteral(".")); | 928 | for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { |
| 904 | WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, | 929 | qt_config->setArrayIndex(i); |
| 905 | false); | 930 | const auto& game_dir = UISettings::values.game_dirs.at(i); |
| 931 | WriteSetting(QStringLiteral("path"), game_dir.path); | ||
| 932 | WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false); | ||
| 933 | WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); | ||
| 934 | } | ||
| 935 | qt_config->endArray(); | ||
| 906 | WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); | 936 | WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); |
| 907 | 937 | ||
| 908 | qt_config->endGroup(); | 938 | qt_config->endGroup(); |
diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index 75fcbfea3..727836b17 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp | |||
| @@ -19,22 +19,17 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) | |||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | SetConfiguration(); | 21 | SetConfiguration(); |
| 22 | |||
| 23 | connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this, | ||
| 24 | [] { UISettings::values.is_game_list_reload_pending.exchange(true); }); | ||
| 25 | } | 22 | } |
| 26 | 23 | ||
| 27 | ConfigureGeneral::~ConfigureGeneral() = default; | 24 | ConfigureGeneral::~ConfigureGeneral() = default; |
| 28 | 25 | ||
| 29 | void ConfigureGeneral::SetConfiguration() { | 26 | void ConfigureGeneral::SetConfiguration() { |
| 30 | ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan); | ||
| 31 | ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); | 27 | ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); |
| 32 | ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot); | 28 | ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot); |
| 33 | ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); | 29 | ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); |
| 34 | } | 30 | } |
| 35 | 31 | ||
| 36 | void ConfigureGeneral::ApplyConfiguration() { | 32 | void ConfigureGeneral::ApplyConfiguration() { |
| 37 | UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked(); | ||
| 38 | UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); | 33 | UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); |
| 39 | UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked(); | 34 | UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked(); |
| 40 | UISettings::values.theme = | 35 | UISettings::values.theme = |
diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui index 184fdd329..e747a4ce2 100644 --- a/src/yuzu/configuration/configure_general.ui +++ b/src/yuzu/configuration/configure_general.ui | |||
| @@ -25,13 +25,6 @@ | |||
| 25 | <item> | 25 | <item> |
| 26 | <layout class="QVBoxLayout" name="GeneralVerticalLayout"> | 26 | <layout class="QVBoxLayout" name="GeneralVerticalLayout"> |
| 27 | <item> | 27 | <item> |
| 28 | <widget class="QCheckBox" name="toggle_deepscan"> | ||
| 29 | <property name="text"> | ||
| 30 | <string>Search sub-directories for games</string> | ||
| 31 | </property> | ||
| 32 | </widget> | ||
| 33 | </item> | ||
| 34 | <item> | ||
| 35 | <widget class="QCheckBox" name="toggle_check_exit"> | 28 | <widget class="QCheckBox" name="toggle_check_exit"> |
| 36 | <property name="text"> | 29 | <property name="text"> |
| 37 | <string>Confirm exit while emulation is running</string> | 30 | <string>Confirm exit while emulation is running</string> |
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index d18b96519..65947c59b 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp | |||
| @@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve | |||
| 34 | return QObject::eventFilter(obj, event); | 34 | return QObject::eventFilter(obj, event); |
| 35 | 35 | ||
| 36 | QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); | 36 | QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); |
| 37 | int rowCount = gamelist->tree_view->model()->rowCount(); | ||
| 38 | QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower(); | 37 | QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower(); |
| 39 | 38 | ||
| 40 | // If the searchfield's text hasn't changed special function keys get checked | 39 | // If the searchfield's text hasn't changed special function keys get checked |
| @@ -56,19 +55,9 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve | |||
| 56 | // If there is only one result launch this game | 55 | // If there is only one result launch this game |
| 57 | case Qt::Key_Return: | 56 | case Qt::Key_Return: |
| 58 | case Qt::Key_Enter: { | 57 | case Qt::Key_Enter: { |
| 59 | QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view); | 58 | if (gamelist->search_field->visible == 1) { |
| 60 | QModelIndex root_index = item_model->invisibleRootItem()->index(); | 59 | QString file_path = gamelist->getLastFilterResultItem(); |
| 61 | QStandardItem* child_file; | 60 | |
| 62 | QString file_path; | ||
| 63 | int resultCount = 0; | ||
| 64 | for (int i = 0; i < rowCount; ++i) { | ||
| 65 | if (!gamelist->tree_view->isRowHidden(i, root_index)) { | ||
| 66 | ++resultCount; | ||
| 67 | child_file = gamelist->item_model->item(i, 0); | ||
| 68 | file_path = child_file->data(GameListItemPath::FullPathRole).toString(); | ||
| 69 | } | ||
| 70 | } | ||
| 71 | if (resultCount == 1) { | ||
| 72 | // To avoid loading error dialog loops while confirming them using enter | 61 | // To avoid loading error dialog loops while confirming them using enter |
| 73 | // Also users usually want to run a different game after closing one | 62 | // Also users usually want to run a different game after closing one |
| 74 | gamelist->search_field->edit_filter->clear(); | 63 | gamelist->search_field->edit_filter->clear(); |
| @@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve | |||
| 88 | } | 77 | } |
| 89 | 78 | ||
| 90 | void GameListSearchField::setFilterResult(int visible, int total) { | 79 | void GameListSearchField::setFilterResult(int visible, int total) { |
| 80 | this->visible = visible; | ||
| 81 | this->total = total; | ||
| 82 | |||
| 91 | label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); | 83 | label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); |
| 92 | } | 84 | } |
| 93 | 85 | ||
| 86 | QString GameList::getLastFilterResultItem() { | ||
| 87 | QStandardItem* folder; | ||
| 88 | QStandardItem* child; | ||
| 89 | QString file_path; | ||
| 90 | int folder_count = item_model->rowCount(); | ||
| 91 | for (int i = 0; i < folder_count; ++i) { | ||
| 92 | folder = item_model->item(i, 0); | ||
| 93 | QModelIndex folder_index = folder->index(); | ||
| 94 | int childrenCount = folder->rowCount(); | ||
| 95 | for (int j = 0; j < childrenCount; ++j) { | ||
| 96 | if (!tree_view->isRowHidden(j, folder_index)) { | ||
| 97 | child = folder->child(j, 0); | ||
| 98 | file_path = child->data(GameListItemPath::FullPathRole).toString(); | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | return file_path; | ||
| 103 | } | ||
| 104 | |||
| 94 | void GameListSearchField::clear() { | 105 | void GameListSearchField::clear() { |
| 95 | edit_filter->clear(); | 106 | edit_filter->clear(); |
| 96 | } | 107 | } |
| @@ -147,45 +158,112 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput) | |||
| 147 | [&haystack](const QString& s) { return haystack.contains(s); }); | 158 | [&haystack](const QString& s) { return haystack.contains(s); }); |
| 148 | } | 159 | } |
| 149 | 160 | ||
| 161 | // Syncs the expanded state of Game Directories with settings to persist across sessions | ||
| 162 | void GameList::onItemExpanded(const QModelIndex& item) { | ||
| 163 | GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>(); | ||
| 164 | if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir || | ||
| 165 | type == GameListItemType::SystemDir) | ||
| 166 | item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded = | ||
| 167 | tree_view->isExpanded(item); | ||
| 168 | } | ||
| 169 | |||
| 150 | // Event in order to filter the gamelist after editing the searchfield | 170 | // Event in order to filter the gamelist after editing the searchfield |
| 151 | void GameList::onTextChanged(const QString& new_text) { | 171 | void GameList::onTextChanged(const QString& new_text) { |
| 152 | const int row_count = tree_view->model()->rowCount(); | 172 | int folder_count = tree_view->model()->rowCount(); |
| 153 | const QString edit_filter_text = new_text.toLower(); | 173 | QString edit_filter_text = new_text.toLower(); |
| 154 | const QModelIndex root_index = item_model->invisibleRootItem()->index(); | 174 | QStandardItem* folder; |
| 175 | QStandardItem* child; | ||
| 176 | int childrenTotal = 0; | ||
| 177 | QModelIndex root_index = item_model->invisibleRootItem()->index(); | ||
| 155 | 178 | ||
| 156 | // If the searchfield is empty every item is visible | 179 | // If the searchfield is empty every item is visible |
| 157 | // Otherwise the filter gets applied | 180 | // Otherwise the filter gets applied |
| 158 | if (edit_filter_text.isEmpty()) { | 181 | if (edit_filter_text.isEmpty()) { |
| 159 | for (int i = 0; i < row_count; ++i) { | 182 | for (int i = 0; i < folder_count; ++i) { |
| 160 | tree_view->setRowHidden(i, root_index, false); | 183 | folder = item_model->item(i, 0); |
| 184 | QModelIndex folder_index = folder->index(); | ||
| 185 | int childrenCount = folder->rowCount(); | ||
| 186 | for (int j = 0; j < childrenCount; ++j) { | ||
| 187 | ++childrenTotal; | ||
| 188 | tree_view->setRowHidden(j, folder_index, false); | ||
| 189 | } | ||
| 161 | } | 190 | } |
| 162 | search_field->setFilterResult(row_count, row_count); | 191 | search_field->setFilterResult(childrenTotal, childrenTotal); |
| 163 | } else { | 192 | } else { |
| 164 | int result_count = 0; | 193 | int result_count = 0; |
| 165 | for (int i = 0; i < row_count; ++i) { | 194 | for (int i = 0; i < folder_count; ++i) { |
| 166 | const QStandardItem* child_file = item_model->item(i, 0); | 195 | folder = item_model->item(i, 0); |
| 167 | const QString file_path = | 196 | QModelIndex folder_index = folder->index(); |
| 168 | child_file->data(GameListItemPath::FullPathRole).toString().toLower(); | 197 | int childrenCount = folder->rowCount(); |
| 169 | const QString file_title = | 198 | for (int j = 0; j < childrenCount; ++j) { |
| 170 | child_file->data(GameListItemPath::TitleRole).toString().toLower(); | 199 | ++childrenTotal; |
| 171 | const QString file_program_id = | 200 | const QStandardItem* child = folder->child(j, 0); |
| 172 | child_file->data(GameListItemPath::ProgramIdRole).toString().toLower(); | 201 | const QString file_path = |
| 173 | 202 | child->data(GameListItemPath::FullPathRole).toString().toLower(); | |
| 174 | // Only items which filename in combination with its title contains all words | 203 | const QString file_title = |
| 175 | // that are in the searchfield will be visible in the gamelist | 204 | child->data(GameListItemPath::TitleRole).toString().toLower(); |
| 176 | // The search is case insensitive because of toLower() | 205 | const QString file_program_id = |
| 177 | // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent | 206 | child->data(GameListItemPath::ProgramIdRole).toString().toLower(); |
| 178 | // multiple conversions of edit_filter_text for each game in the gamelist | 207 | |
| 179 | const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + | 208 | // Only items which filename in combination with its title contains all words |
| 180 | QLatin1Char{' '} + file_title; | 209 | // that are in the searchfield will be visible in the gamelist |
| 181 | if (ContainsAllWords(file_name, edit_filter_text) || | 210 | // The search is case insensitive because of toLower() |
| 182 | (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { | 211 | // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent |
| 183 | tree_view->setRowHidden(i, root_index, false); | 212 | // multiple conversions of edit_filter_text for each game in the gamelist |
| 184 | ++result_count; | 213 | const QString file_name = |
| 185 | } else { | 214 | file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + |
| 186 | tree_view->setRowHidden(i, root_index, true); | 215 | file_title; |
| 216 | if (ContainsAllWords(file_name, edit_filter_text) || | ||
| 217 | (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { | ||
| 218 | tree_view->setRowHidden(j, folder_index, false); | ||
| 219 | ++result_count; | ||
| 220 | } else { | ||
| 221 | tree_view->setRowHidden(j, folder_index, true); | ||
| 222 | } | ||
| 223 | search_field->setFilterResult(result_count, childrenTotal); | ||
| 187 | } | 224 | } |
| 188 | search_field->setFilterResult(result_count, row_count); | 225 | } |
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | void GameList::onUpdateThemedIcons() { | ||
| 230 | for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) { | ||
| 231 | QStandardItem* child = item_model->invisibleRootItem()->child(i); | ||
| 232 | |||
| 233 | int icon_size = UISettings::values.icon_size; | ||
| 234 | switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) { | ||
| 235 | case GameListItemType::InstalledDir: | ||
| 236 | child->setData( | ||
| 237 | QIcon::fromTheme(QStringLiteral("sd_card")) | ||
| 238 | .pixmap(icon_size) | ||
| 239 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 240 | Qt::DecorationRole); | ||
| 241 | break; | ||
| 242 | case GameListItemType::SystemDir: | ||
| 243 | child->setData( | ||
| 244 | QIcon::fromTheme(QStringLiteral("chip")) | ||
| 245 | .pixmap(icon_size) | ||
| 246 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 247 | Qt::DecorationRole); | ||
| 248 | break; | ||
| 249 | case GameListItemType::CustomDir: { | ||
| 250 | const UISettings::GameDir* game_dir = | ||
| 251 | child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 252 | QString icon_name = QFileInfo::exists(game_dir->path) ? QStringLiteral("folder") | ||
| 253 | : QStringLiteral("bad_folder"); | ||
| 254 | child->setData( | ||
| 255 | QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( | ||
| 256 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 257 | Qt::DecorationRole); | ||
| 258 | break; | ||
| 259 | } | ||
| 260 | case GameListItemType::AddDir: | ||
| 261 | child->setData( | ||
| 262 | QIcon::fromTheme(QStringLiteral("plus")) | ||
| 263 | .pixmap(icon_size) | ||
| 264 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 265 | Qt::DecorationRole); | ||
| 266 | break; | ||
| 189 | } | 267 | } |
| 190 | } | 268 | } |
| 191 | } | 269 | } |
| @@ -230,12 +308,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide | |||
| 230 | item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); | 308 | item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); |
| 231 | item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); | 309 | item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); |
| 232 | } | 310 | } |
| 311 | item_model->setSortRole(GameListItemPath::TitleRole); | ||
| 233 | 312 | ||
| 313 | connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons); | ||
| 234 | connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); | 314 | connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); |
| 235 | connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); | 315 | connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); |
| 316 | connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded); | ||
| 317 | connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded); | ||
| 236 | 318 | ||
| 237 | // We must register all custom types with the Qt Automoc system so that we are able to use it | 319 | // We must register all custom types with the Qt Automoc system so that we are able to use |
| 238 | // with signals/slots. In this case, QList falls under the umbrells of custom types. | 320 | // it with signals/slots. In this case, QList falls under the umbrells of custom types. |
| 239 | qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); | 321 | qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); |
| 240 | 322 | ||
| 241 | layout->setContentsMargins(0, 0, 0, 0); | 323 | layout->setContentsMargins(0, 0, 0, 0); |
| @@ -263,38 +345,67 @@ void GameList::clearFilter() { | |||
| 263 | search_field->clear(); | 345 | search_field->clear(); |
| 264 | } | 346 | } |
| 265 | 347 | ||
| 266 | void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { | 348 | void GameList::AddDirEntry(GameListDir* entry_items) { |
| 267 | item_model->invisibleRootItem()->appendRow(entry_items); | 349 | item_model->invisibleRootItem()->appendRow(entry_items); |
| 350 | tree_view->setExpanded( | ||
| 351 | entry_items->index(), | ||
| 352 | entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded); | ||
| 268 | } | 353 | } |
| 269 | 354 | ||
| 270 | void GameList::ValidateEntry(const QModelIndex& item) { | 355 | void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) { |
| 271 | // We don't care about the individual QStandardItem that was selected, but its row. | 356 | parent->appendRow(entry_items); |
| 272 | const int row = item_model->itemFromIndex(item)->row(); | 357 | } |
| 273 | const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); | ||
| 274 | const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString(); | ||
| 275 | |||
| 276 | if (file_path.isEmpty()) | ||
| 277 | return; | ||
| 278 | |||
| 279 | if (!QFileInfo::exists(file_path)) | ||
| 280 | return; | ||
| 281 | 358 | ||
| 282 | const QFileInfo file_info{file_path}; | 359 | void GameList::ValidateEntry(const QModelIndex& item) { |
| 283 | if (file_info.isDir()) { | 360 | auto selected = item.sibling(item.row(), 0); |
| 284 | const QDir dir{file_path}; | 361 | |
| 285 | const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); | 362 | switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { |
| 286 | if (matching_main.size() == 1) { | 363 | case GameListItemType::Game: { |
| 287 | emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); | 364 | QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); |
| 365 | if (file_path.isEmpty()) | ||
| 366 | return; | ||
| 367 | QFileInfo file_info(file_path); | ||
| 368 | if (!file_info.exists()) | ||
| 369 | return; | ||
| 370 | |||
| 371 | if (file_info.isDir()) { | ||
| 372 | const QDir dir{file_path}; | ||
| 373 | const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); | ||
| 374 | if (matching_main.size() == 1) { | ||
| 375 | emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); | ||
| 376 | } | ||
| 377 | return; | ||
| 288 | } | 378 | } |
| 289 | return; | 379 | |
| 380 | // Users usually want to run a different game after closing one | ||
| 381 | search_field->clear(); | ||
| 382 | emit GameChosen(file_path); | ||
| 383 | break; | ||
| 290 | } | 384 | } |
| 385 | case GameListItemType::AddDir: | ||
| 386 | emit AddDirectory(); | ||
| 387 | break; | ||
| 388 | } | ||
| 389 | } | ||
| 291 | 390 | ||
| 292 | // Users usually want to run a diffrent game after closing one | 391 | bool GameList::isEmpty() { |
| 293 | search_field->clear(); | 392 | for (int i = 0; i < item_model->rowCount(); i++) { |
| 294 | emit GameChosen(file_path); | 393 | const QStandardItem* child = item_model->invisibleRootItem()->child(i); |
| 394 | GameListItemType type = static_cast<GameListItemType>(child->type()); | ||
| 395 | if (!child->hasChildren() && | ||
| 396 | (type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) { | ||
| 397 | item_model->invisibleRootItem()->removeRow(child->row()); | ||
| 398 | i--; | ||
| 399 | }; | ||
| 400 | } | ||
| 401 | return !item_model->invisibleRootItem()->hasChildren(); | ||
| 295 | } | 402 | } |
| 296 | 403 | ||
| 297 | void GameList::DonePopulating(QStringList watch_list) { | 404 | void GameList::DonePopulating(QStringList watch_list) { |
| 405 | emit ShowList(!isEmpty()); | ||
| 406 | |||
| 407 | item_model->invisibleRootItem()->appendRow(new GameListAddDir()); | ||
| 408 | |||
| 298 | // Clear out the old directories to watch for changes and add the new ones | 409 | // Clear out the old directories to watch for changes and add the new ones |
| 299 | auto watch_dirs = watcher->directories(); | 410 | auto watch_dirs = watcher->directories(); |
| 300 | if (!watch_dirs.isEmpty()) { | 411 | if (!watch_dirs.isEmpty()) { |
| @@ -311,9 +422,16 @@ void GameList::DonePopulating(QStringList watch_list) { | |||
| 311 | QCoreApplication::processEvents(); | 422 | QCoreApplication::processEvents(); |
| 312 | } | 423 | } |
| 313 | tree_view->setEnabled(true); | 424 | tree_view->setEnabled(true); |
| 314 | int rowCount = tree_view->model()->rowCount(); | 425 | int folder_count = tree_view->model()->rowCount(); |
| 315 | search_field->setFilterResult(rowCount, rowCount); | 426 | int childrenTotal = 0; |
| 316 | if (rowCount > 0) { | 427 | for (int i = 0; i < folder_count; ++i) { |
| 428 | int childrenCount = item_model->item(i, 0)->rowCount(); | ||
| 429 | for (int j = 0; j < childrenCount; ++j) { | ||
| 430 | ++childrenTotal; | ||
| 431 | } | ||
| 432 | } | ||
| 433 | search_field->setFilterResult(childrenTotal, childrenTotal); | ||
| 434 | if (childrenTotal > 0) { | ||
| 317 | search_field->setFocus(); | 435 | search_field->setFocus(); |
| 318 | } | 436 | } |
| 319 | } | 437 | } |
| @@ -323,12 +441,26 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
| 323 | if (!item.isValid()) | 441 | if (!item.isValid()) |
| 324 | return; | 442 | return; |
| 325 | 443 | ||
| 326 | int row = item_model->itemFromIndex(item)->row(); | 444 | auto selected = item.sibling(item.row(), 0); |
| 327 | QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); | ||
| 328 | u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong(); | ||
| 329 | std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString(); | ||
| 330 | |||
| 331 | QMenu context_menu; | 445 | QMenu context_menu; |
| 446 | switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||
| 447 | case GameListItemType::Game: | ||
| 448 | AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(), | ||
| 449 | selected.data(GameListItemPath::FullPathRole).toString().toStdString()); | ||
| 450 | break; | ||
| 451 | case GameListItemType::CustomDir: | ||
| 452 | AddPermDirPopup(context_menu, selected); | ||
| 453 | AddCustomDirPopup(context_menu, selected); | ||
| 454 | break; | ||
| 455 | case GameListItemType::InstalledDir: | ||
| 456 | case GameListItemType::SystemDir: | ||
| 457 | AddPermDirPopup(context_menu, selected); | ||
| 458 | break; | ||
| 459 | } | ||
| 460 | context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | ||
| 461 | } | ||
| 462 | |||
| 463 | void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) { | ||
| 332 | QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); | 464 | QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); |
| 333 | QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); | 465 | QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); |
| 334 | QAction* open_transferable_shader_cache = | 466 | QAction* open_transferable_shader_cache = |
| @@ -344,19 +476,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
| 344 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | 476 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); |
| 345 | navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); | 477 | navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); |
| 346 | 478 | ||
| 347 | connect(open_save_location, &QAction::triggered, | 479 | connect(open_save_location, &QAction::triggered, [this, program_id]() { |
| 348 | [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); | 480 | emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); |
| 349 | connect(open_lfs_location, &QAction::triggered, | 481 | }); |
| 350 | [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); | 482 | connect(open_lfs_location, &QAction::triggered, [this, program_id]() { |
| 483 | emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); | ||
| 484 | }); | ||
| 351 | connect(open_transferable_shader_cache, &QAction::triggered, | 485 | connect(open_transferable_shader_cache, &QAction::triggered, |
| 352 | [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); | 486 | [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); |
| 353 | connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); | 487 | connect(dump_romfs, &QAction::triggered, |
| 354 | connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); | 488 | [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); }); |
| 355 | connect(navigate_to_gamedb_entry, &QAction::triggered, | 489 | connect(copy_tid, &QAction::triggered, |
| 356 | [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); | 490 | [this, program_id]() { emit CopyTIDRequested(program_id); }); |
| 357 | connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); | 491 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { |
| 492 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); | ||
| 493 | }); | ||
| 494 | connect(properties, &QAction::triggered, | ||
| 495 | [this, path]() { emit OpenPerGameGeneralRequested(path); }); | ||
| 496 | }; | ||
| 497 | |||
| 498 | void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { | ||
| 499 | UISettings::GameDir& game_dir = | ||
| 500 | *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 501 | |||
| 502 | QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); | ||
| 503 | QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); | ||
| 504 | |||
| 505 | deep_scan->setCheckable(true); | ||
| 506 | deep_scan->setChecked(game_dir.deep_scan); | ||
| 507 | |||
| 508 | connect(deep_scan, &QAction::triggered, [this, &game_dir] { | ||
| 509 | game_dir.deep_scan = !game_dir.deep_scan; | ||
| 510 | PopulateAsync(UISettings::values.game_dirs); | ||
| 511 | }); | ||
| 512 | connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { | ||
| 513 | UISettings::values.game_dirs.removeOne(game_dir); | ||
| 514 | item_model->invisibleRootItem()->removeRow(selected.row()); | ||
| 515 | }); | ||
| 516 | } | ||
| 358 | 517 | ||
| 359 | context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | 518 | void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { |
| 519 | UISettings::GameDir& game_dir = | ||
| 520 | *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 521 | |||
| 522 | QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up")); | ||
| 523 | QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down ")); | ||
| 524 | QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location")); | ||
| 525 | |||
| 526 | int row = selected.row(); | ||
| 527 | |||
| 528 | move_up->setEnabled(row > 0); | ||
| 529 | move_down->setEnabled(row < item_model->rowCount() - 2); | ||
| 530 | |||
| 531 | connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] { | ||
| 532 | // find the indices of the items in settings and swap them | ||
| 533 | UISettings::values.game_dirs.swap( | ||
| 534 | UISettings::values.game_dirs.indexOf(game_dir), | ||
| 535 | UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() - 1, 0) | ||
| 536 | .data(GameListDir::GameDirRole) | ||
| 537 | .value<UISettings::GameDir*>())); | ||
| 538 | // move the treeview items | ||
| 539 | QList<QStandardItem*> item = item_model->takeRow(row); | ||
| 540 | item_model->invisibleRootItem()->insertRow(row - 1, item); | ||
| 541 | tree_view->setExpanded(selected, game_dir.expanded); | ||
| 542 | }); | ||
| 543 | |||
| 544 | connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] { | ||
| 545 | // find the indices of the items in settings and swap them | ||
| 546 | UISettings::values.game_dirs.swap( | ||
| 547 | UISettings::values.game_dirs.indexOf(game_dir), | ||
| 548 | UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() + 1, 0) | ||
| 549 | .data(GameListDir::GameDirRole) | ||
| 550 | .value<UISettings::GameDir*>())); | ||
| 551 | // move the treeview items | ||
| 552 | QList<QStandardItem*> item = item_model->takeRow(row); | ||
| 553 | item_model->invisibleRootItem()->insertRow(row + 1, item); | ||
| 554 | tree_view->setExpanded(selected, game_dir.expanded); | ||
| 555 | }); | ||
| 556 | |||
| 557 | connect(open_directory_location, &QAction::triggered, | ||
| 558 | [this, game_dir] { emit OpenDirectory(game_dir.path); }); | ||
| 360 | } | 559 | } |
| 361 | 560 | ||
| 362 | void GameList::LoadCompatibilityList() { | 561 | void GameList::LoadCompatibilityList() { |
| @@ -403,14 +602,7 @@ void GameList::LoadCompatibilityList() { | |||
| 403 | } | 602 | } |
| 404 | } | 603 | } |
| 405 | 604 | ||
| 406 | void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | 605 | void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) { |
| 407 | const QFileInfo dir_info{dir_path}; | ||
| 408 | if (!dir_info.exists() || !dir_info.isDir()) { | ||
| 409 | LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString()); | ||
| 410 | search_field->setFilterResult(0, 0); | ||
| 411 | return; | ||
| 412 | } | ||
| 413 | |||
| 414 | tree_view->setEnabled(false); | 606 | tree_view->setEnabled(false); |
| 415 | 607 | ||
| 416 | // Update the columns in case UISettings has changed | 608 | // Update the columns in case UISettings has changed |
| @@ -433,17 +625,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | |||
| 433 | 625 | ||
| 434 | // Delete any rows that might already exist if we're repopulating | 626 | // Delete any rows that might already exist if we're repopulating |
| 435 | item_model->removeRows(0, item_model->rowCount()); | 627 | item_model->removeRows(0, item_model->rowCount()); |
| 628 | search_field->clear(); | ||
| 436 | 629 | ||
| 437 | emit ShouldCancelWorker(); | 630 | emit ShouldCancelWorker(); |
| 438 | 631 | ||
| 439 | GameListWorker* worker = | 632 | GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list); |
| 440 | new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list); | ||
| 441 | 633 | ||
| 442 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); | 634 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); |
| 635 | connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, | ||
| 636 | Qt::QueuedConnection); | ||
| 443 | connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, | 637 | connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, |
| 444 | Qt::QueuedConnection); | 638 | Qt::QueuedConnection); |
| 445 | // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel | 639 | // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to |
| 446 | // without delay. | 640 | // cancel without delay. |
| 447 | connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, | 641 | connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, |
| 448 | Qt::DirectConnection); | 642 | Qt::DirectConnection); |
| 449 | 643 | ||
| @@ -471,10 +665,42 @@ const QStringList GameList::supported_file_extensions = { | |||
| 471 | QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; | 665 | QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; |
| 472 | 666 | ||
| 473 | void GameList::RefreshGameDirectory() { | 667 | void GameList::RefreshGameDirectory() { |
| 474 | if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { | 668 | if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { |
| 475 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); | 669 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); |
| 476 | search_field->clear(); | 670 | PopulateAsync(UISettings::values.game_dirs); |
| 477 | PopulateAsync(UISettings::values.game_directory_path, | ||
| 478 | UISettings::values.game_directory_deepscan); | ||
| 479 | } | 671 | } |
| 480 | } | 672 | } |
| 673 | |||
| 674 | GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { | ||
| 675 | this->main_window = parent; | ||
| 676 | |||
| 677 | connect(main_window, &GMainWindow::UpdateThemedIcons, this, | ||
| 678 | &GameListPlaceholder::onUpdateThemedIcons); | ||
| 679 | |||
| 680 | layout = new QVBoxLayout; | ||
| 681 | image = new QLabel; | ||
| 682 | text = new QLabel; | ||
| 683 | layout->setAlignment(Qt::AlignCenter); | ||
| 684 | image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); | ||
| 685 | |||
| 686 | text->setText(tr("Double-click to add a new folder to the game list ")); | ||
| 687 | QFont font = text->font(); | ||
| 688 | font.setPointSize(20); | ||
| 689 | text->setFont(font); | ||
| 690 | text->setAlignment(Qt::AlignHCenter); | ||
| 691 | image->setAlignment(Qt::AlignHCenter); | ||
| 692 | |||
| 693 | layout->addWidget(image); | ||
| 694 | layout->addWidget(text); | ||
| 695 | setLayout(layout); | ||
| 696 | } | ||
| 697 | |||
| 698 | GameListPlaceholder::~GameListPlaceholder() = default; | ||
| 699 | |||
| 700 | void GameListPlaceholder::onUpdateThemedIcons() { | ||
| 701 | image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); | ||
| 702 | } | ||
| 703 | |||
| 704 | void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { | ||
| 705 | emit GameListPlaceholder::AddDirectory(); | ||
| 706 | } | ||
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index f8f8bd6c5..a2b58aba5 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h | |||
| @@ -19,10 +19,14 @@ | |||
| 19 | #include <QWidget> | 19 | #include <QWidget> |
| 20 | 20 | ||
| 21 | #include "common/common_types.h" | 21 | #include "common/common_types.h" |
| 22 | #include "ui_settings.h" | ||
| 22 | #include "yuzu/compatibility_list.h" | 23 | #include "yuzu/compatibility_list.h" |
| 23 | 24 | ||
| 24 | class GameListWorker; | 25 | class GameListWorker; |
| 25 | class GameListSearchField; | 26 | class GameListSearchField; |
| 27 | template <typename> | ||
| 28 | class QList; | ||
| 29 | class GameListDir; | ||
| 26 | class GMainWindow; | 30 | class GMainWindow; |
| 27 | 31 | ||
| 28 | namespace FileSys { | 32 | namespace FileSys { |
| @@ -52,12 +56,14 @@ public: | |||
| 52 | FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr); | 56 | FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr); |
| 53 | ~GameList() override; | 57 | ~GameList() override; |
| 54 | 58 | ||
| 59 | QString getLastFilterResultItem(); | ||
| 55 | void clearFilter(); | 60 | void clearFilter(); |
| 56 | void setFilterFocus(); | 61 | void setFilterFocus(); |
| 57 | void setFilterVisible(bool visibility); | 62 | void setFilterVisible(bool visibility); |
| 63 | bool isEmpty(); | ||
| 58 | 64 | ||
| 59 | void LoadCompatibilityList(); | 65 | void LoadCompatibilityList(); |
| 60 | void PopulateAsync(const QString& dir_path, bool deep_scan); | 66 | void PopulateAsync(QList<UISettings::GameDir>& game_dirs); |
| 61 | 67 | ||
| 62 | void SaveInterfaceLayout(); | 68 | void SaveInterfaceLayout(); |
| 63 | void LoadInterfaceLayout(); | 69 | void LoadInterfaceLayout(); |
| @@ -74,19 +80,29 @@ signals: | |||
| 74 | void NavigateToGamedbEntryRequested(u64 program_id, | 80 | void NavigateToGamedbEntryRequested(u64 program_id, |
| 75 | const CompatibilityList& compatibility_list); | 81 | const CompatibilityList& compatibility_list); |
| 76 | void OpenPerGameGeneralRequested(const std::string& file); | 82 | void OpenPerGameGeneralRequested(const std::string& file); |
| 83 | void OpenDirectory(QString directory); | ||
| 84 | void AddDirectory(); | ||
| 85 | void ShowList(bool show); | ||
| 77 | 86 | ||
| 78 | private slots: | 87 | private slots: |
| 88 | void onItemExpanded(const QModelIndex& item); | ||
| 79 | void onTextChanged(const QString& new_text); | 89 | void onTextChanged(const QString& new_text); |
| 80 | void onFilterCloseClicked(); | 90 | void onFilterCloseClicked(); |
| 91 | void onUpdateThemedIcons(); | ||
| 81 | 92 | ||
| 82 | private: | 93 | private: |
| 83 | void AddEntry(const QList<QStandardItem*>& entry_items); | 94 | void AddDirEntry(GameListDir* entry_items); |
| 95 | void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent); | ||
| 84 | void ValidateEntry(const QModelIndex& item); | 96 | void ValidateEntry(const QModelIndex& item); |
| 85 | void DonePopulating(QStringList watch_list); | 97 | void DonePopulating(QStringList watch_list); |
| 86 | 98 | ||
| 87 | void PopupContextMenu(const QPoint& menu_location); | ||
| 88 | void RefreshGameDirectory(); | 99 | void RefreshGameDirectory(); |
| 89 | 100 | ||
| 101 | void PopupContextMenu(const QPoint& menu_location); | ||
| 102 | void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path); | ||
| 103 | void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); | ||
| 104 | void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); | ||
| 105 | |||
| 90 | std::shared_ptr<FileSys::VfsFilesystem> vfs; | 106 | std::shared_ptr<FileSys::VfsFilesystem> vfs; |
| 91 | FileSys::ManualContentProvider* provider; | 107 | FileSys::ManualContentProvider* provider; |
| 92 | GameListSearchField* search_field; | 108 | GameListSearchField* search_field; |
| @@ -102,3 +118,25 @@ private: | |||
| 102 | }; | 118 | }; |
| 103 | 119 | ||
| 104 | Q_DECLARE_METATYPE(GameListOpenTarget); | 120 | Q_DECLARE_METATYPE(GameListOpenTarget); |
| 121 | |||
| 122 | class GameListPlaceholder : public QWidget { | ||
| 123 | Q_OBJECT | ||
| 124 | public: | ||
| 125 | explicit GameListPlaceholder(GMainWindow* parent = nullptr); | ||
| 126 | ~GameListPlaceholder(); | ||
| 127 | |||
| 128 | signals: | ||
| 129 | void AddDirectory(); | ||
| 130 | |||
| 131 | private slots: | ||
| 132 | void onUpdateThemedIcons(); | ||
| 133 | |||
| 134 | protected: | ||
| 135 | void mouseDoubleClickEvent(QMouseEvent* event) override; | ||
| 136 | |||
| 137 | private: | ||
| 138 | GMainWindow* main_window = nullptr; | ||
| 139 | QVBoxLayout* layout = nullptr; | ||
| 140 | QLabel* image = nullptr; | ||
| 141 | QLabel* text = nullptr; | ||
| 142 | }; | ||
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index ece534dd6..f5abb759d 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h | |||
| @@ -10,6 +10,7 @@ | |||
| 10 | #include <utility> | 10 | #include <utility> |
| 11 | 11 | ||
| 12 | #include <QCoreApplication> | 12 | #include <QCoreApplication> |
| 13 | #include <QFileInfo> | ||
| 13 | #include <QImage> | 14 | #include <QImage> |
| 14 | #include <QObject> | 15 | #include <QObject> |
| 15 | #include <QStandardItem> | 16 | #include <QStandardItem> |
| @@ -22,6 +23,16 @@ | |||
| 22 | #include "yuzu/uisettings.h" | 23 | #include "yuzu/uisettings.h" |
| 23 | #include "yuzu/util/util.h" | 24 | #include "yuzu/util/util.h" |
| 24 | 25 | ||
| 26 | enum class GameListItemType { | ||
| 27 | Game = QStandardItem::UserType + 1, | ||
| 28 | CustomDir = QStandardItem::UserType + 2, | ||
| 29 | InstalledDir = QStandardItem::UserType + 3, | ||
| 30 | SystemDir = QStandardItem::UserType + 4, | ||
| 31 | AddDir = QStandardItem::UserType + 5 | ||
| 32 | }; | ||
| 33 | |||
| 34 | Q_DECLARE_METATYPE(GameListItemType); | ||
| 35 | |||
| 25 | /** | 36 | /** |
| 26 | * Gets the default icon (for games without valid title metadata) | 37 | * Gets the default icon (for games without valid title metadata) |
| 27 | * @param size The desired width and height of the default icon. | 38 | * @param size The desired width and height of the default icon. |
| @@ -36,8 +47,13 @@ static QPixmap GetDefaultIcon(u32 size) { | |||
| 36 | class GameListItem : public QStandardItem { | 47 | class GameListItem : public QStandardItem { |
| 37 | 48 | ||
| 38 | public: | 49 | public: |
| 50 | // used to access type from item index | ||
| 51 | static const int TypeRole = Qt::UserRole + 1; | ||
| 52 | static const int SortRole = Qt::UserRole + 2; | ||
| 39 | GameListItem() = default; | 53 | GameListItem() = default; |
| 40 | explicit GameListItem(const QString& string) : QStandardItem(string) {} | 54 | GameListItem(const QString& string) : QStandardItem(string) { |
| 55 | setData(string, SortRole); | ||
| 56 | } | ||
| 41 | }; | 57 | }; |
| 42 | 58 | ||
| 43 | /** | 59 | /** |
| @@ -48,14 +64,15 @@ public: | |||
| 48 | */ | 64 | */ |
| 49 | class GameListItemPath : public GameListItem { | 65 | class GameListItemPath : public GameListItem { |
| 50 | public: | 66 | public: |
| 51 | static const int FullPathRole = Qt::UserRole + 1; | 67 | static const int TitleRole = SortRole; |
| 52 | static const int TitleRole = Qt::UserRole + 2; | 68 | static const int FullPathRole = SortRole + 1; |
| 53 | static const int ProgramIdRole = Qt::UserRole + 3; | 69 | static const int ProgramIdRole = SortRole + 2; |
| 54 | static const int FileTypeRole = Qt::UserRole + 4; | 70 | static const int FileTypeRole = SortRole + 3; |
| 55 | 71 | ||
| 56 | GameListItemPath() = default; | 72 | GameListItemPath() = default; |
| 57 | GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, | 73 | GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, |
| 58 | const QString& game_name, const QString& game_type, u64 program_id) { | 74 | const QString& game_name, const QString& game_type, u64 program_id) { |
| 75 | setData(type(), TypeRole); | ||
| 59 | setData(game_path, FullPathRole); | 76 | setData(game_path, FullPathRole); |
| 60 | setData(game_name, TitleRole); | 77 | setData(game_name, TitleRole); |
| 61 | setData(qulonglong(program_id), ProgramIdRole); | 78 | setData(qulonglong(program_id), ProgramIdRole); |
| @@ -72,6 +89,10 @@ public: | |||
| 72 | setData(picture, Qt::DecorationRole); | 89 | setData(picture, Qt::DecorationRole); |
| 73 | } | 90 | } |
| 74 | 91 | ||
| 92 | int type() const override { | ||
| 93 | return static_cast<int>(GameListItemType::Game); | ||
| 94 | } | ||
| 95 | |||
| 75 | QVariant data(int role) const override { | 96 | QVariant data(int role) const override { |
| 76 | if (role == Qt::DisplayRole) { | 97 | if (role == Qt::DisplayRole) { |
| 77 | std::string filename; | 98 | std::string filename; |
| @@ -103,9 +124,11 @@ public: | |||
| 103 | class GameListItemCompat : public GameListItem { | 124 | class GameListItemCompat : public GameListItem { |
| 104 | Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) | 125 | Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) |
| 105 | public: | 126 | public: |
| 106 | static const int CompatNumberRole = Qt::UserRole + 1; | 127 | static const int CompatNumberRole = SortRole; |
| 107 | GameListItemCompat() = default; | 128 | GameListItemCompat() = default; |
| 108 | explicit GameListItemCompat(const QString& compatibility) { | 129 | explicit GameListItemCompat(const QString& compatibility) { |
| 130 | setData(type(), TypeRole); | ||
| 131 | |||
| 109 | struct CompatStatus { | 132 | struct CompatStatus { |
| 110 | QString color; | 133 | QString color; |
| 111 | const char* text; | 134 | const char* text; |
| @@ -135,6 +158,10 @@ public: | |||
| 135 | setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); | 158 | setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); |
| 136 | } | 159 | } |
| 137 | 160 | ||
| 161 | int type() const override { | ||
| 162 | return static_cast<int>(GameListItemType::Game); | ||
| 163 | } | ||
| 164 | |||
| 138 | bool operator<(const QStandardItem& other) const override { | 165 | bool operator<(const QStandardItem& other) const override { |
| 139 | return data(CompatNumberRole) < other.data(CompatNumberRole); | 166 | return data(CompatNumberRole) < other.data(CompatNumberRole); |
| 140 | } | 167 | } |
| @@ -146,12 +173,12 @@ public: | |||
| 146 | * human-readable string representation will be displayed to the user. | 173 | * human-readable string representation will be displayed to the user. |
| 147 | */ | 174 | */ |
| 148 | class GameListItemSize : public GameListItem { | 175 | class GameListItemSize : public GameListItem { |
| 149 | |||
| 150 | public: | 176 | public: |
| 151 | static const int SizeRole = Qt::UserRole + 1; | 177 | static const int SizeRole = SortRole; |
| 152 | 178 | ||
| 153 | GameListItemSize() = default; | 179 | GameListItemSize() = default; |
| 154 | explicit GameListItemSize(const qulonglong size_bytes) { | 180 | explicit GameListItemSize(const qulonglong size_bytes) { |
| 181 | setData(type(), TypeRole); | ||
| 155 | setData(size_bytes, SizeRole); | 182 | setData(size_bytes, SizeRole); |
| 156 | } | 183 | } |
| 157 | 184 | ||
| @@ -167,6 +194,10 @@ public: | |||
| 167 | } | 194 | } |
| 168 | } | 195 | } |
| 169 | 196 | ||
| 197 | int type() const override { | ||
| 198 | return static_cast<int>(GameListItemType::Game); | ||
| 199 | } | ||
| 200 | |||
| 170 | /** | 201 | /** |
| 171 | * This operator is, in practice, only used by the TreeView sorting systems. | 202 | * This operator is, in practice, only used by the TreeView sorting systems. |
| 172 | * Override it so that it will correctly sort by numerical value instead of by string | 203 | * Override it so that it will correctly sort by numerical value instead of by string |
| @@ -177,6 +208,67 @@ public: | |||
| 177 | } | 208 | } |
| 178 | }; | 209 | }; |
| 179 | 210 | ||
| 211 | class GameListDir : public GameListItem { | ||
| 212 | public: | ||
| 213 | static const int GameDirRole = Qt::UserRole + 2; | ||
| 214 | |||
| 215 | explicit GameListDir(UISettings::GameDir& directory, | ||
| 216 | GameListItemType dir_type = GameListItemType::CustomDir) | ||
| 217 | : dir_type{dir_type} { | ||
| 218 | setData(type(), TypeRole); | ||
| 219 | |||
| 220 | UISettings::GameDir* game_dir = &directory; | ||
| 221 | setData(QVariant::fromValue(game_dir), GameDirRole); | ||
| 222 | |||
| 223 | int icon_size = UISettings::values.icon_size; | ||
| 224 | switch (dir_type) { | ||
| 225 | case GameListItemType::InstalledDir: | ||
| 226 | setData(QIcon::fromTheme("sd_card").pixmap(icon_size).scaled( | ||
| 227 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 228 | Qt::DecorationRole); | ||
| 229 | setData("Installed Titles", Qt::DisplayRole); | ||
| 230 | break; | ||
| 231 | case GameListItemType::SystemDir: | ||
| 232 | setData(QIcon::fromTheme("chip").pixmap(icon_size).scaled( | ||
| 233 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 234 | Qt::DecorationRole); | ||
| 235 | setData("System Titles", Qt::DisplayRole); | ||
| 236 | break; | ||
| 237 | case GameListItemType::CustomDir: | ||
| 238 | QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder"; | ||
| 239 | setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( | ||
| 240 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 241 | Qt::DecorationRole); | ||
| 242 | setData(game_dir->path, Qt::DisplayRole); | ||
| 243 | break; | ||
| 244 | }; | ||
| 245 | }; | ||
| 246 | |||
| 247 | int type() const override { | ||
| 248 | return static_cast<int>(dir_type); | ||
| 249 | } | ||
| 250 | |||
| 251 | private: | ||
| 252 | GameListItemType dir_type; | ||
| 253 | }; | ||
| 254 | |||
| 255 | class GameListAddDir : public GameListItem { | ||
| 256 | public: | ||
| 257 | explicit GameListAddDir() { | ||
| 258 | setData(type(), TypeRole); | ||
| 259 | |||
| 260 | int icon_size = UISettings::values.icon_size; | ||
| 261 | setData(QIcon::fromTheme("plus").pixmap(icon_size).scaled( | ||
| 262 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 263 | Qt::DecorationRole); | ||
| 264 | setData("Add New Game Directory", Qt::DisplayRole); | ||
| 265 | } | ||
| 266 | |||
| 267 | int type() const override { | ||
| 268 | return static_cast<int>(GameListItemType::AddDir); | ||
| 269 | } | ||
| 270 | }; | ||
| 271 | |||
| 180 | class GameList; | 272 | class GameList; |
| 181 | class QHBoxLayout; | 273 | class QHBoxLayout; |
| 182 | class QTreeView; | 274 | class QTreeView; |
| @@ -195,6 +287,9 @@ public: | |||
| 195 | void clear(); | 287 | void clear(); |
| 196 | void setFocus(); | 288 | void setFocus(); |
| 197 | 289 | ||
| 290 | int visible; | ||
| 291 | int total; | ||
| 292 | |||
| 198 | private: | 293 | private: |
| 199 | class KeyReleaseEater : public QObject { | 294 | class KeyReleaseEater : public QObject { |
| 200 | public: | 295 | public: |
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 77f358630..8c6621c98 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp | |||
| @@ -223,21 +223,38 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri | |||
| 223 | } // Anonymous namespace | 223 | } // Anonymous namespace |
| 224 | 224 | ||
| 225 | GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, | 225 | GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, |
| 226 | FileSys::ManualContentProvider* provider, QString dir_path, | 226 | FileSys::ManualContentProvider* provider, |
| 227 | bool deep_scan, const CompatibilityList& compatibility_list) | 227 | QList<UISettings::GameDir>& game_dirs, |
| 228 | : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan), | 228 | const CompatibilityList& compatibility_list) |
| 229 | : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs), | ||
| 229 | compatibility_list(compatibility_list) {} | 230 | compatibility_list(compatibility_list) {} |
| 230 | 231 | ||
| 231 | GameListWorker::~GameListWorker() = default; | 232 | GameListWorker::~GameListWorker() = default; |
| 232 | 233 | ||
| 233 | void GameListWorker::AddTitlesToGameList() { | 234 | void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { |
| 234 | const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>( | 235 | using namespace FileSys; |
| 235 | Core::System::GetInstance().GetContentProvider()); | 236 | |
| 236 | const auto installed_games = cache.ListEntriesFilterOrigin( | 237 | const auto& cache = |
| 237 | std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); | 238 | dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider()); |
| 239 | |||
| 240 | std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games; | ||
| 241 | installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application, | ||
| 242 | ContentRecordType::Program); | ||
| 243 | if (parent_dir->type() == static_cast<int>(GameListItemType::InstalledDir)) { | ||
| 244 | installed_games = cache.ListEntriesFilterOrigin( | ||
| 245 | ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program); | ||
| 246 | auto installed_sdmc_games = cache.ListEntriesFilterOrigin( | ||
| 247 | ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program); | ||
| 248 | |||
| 249 | installed_games.insert(installed_games.end(), installed_sdmc_games.begin(), | ||
| 250 | installed_sdmc_games.end()); | ||
| 251 | } else if (parent_dir->type() == static_cast<int>(GameListItemType::SystemDir)) { | ||
| 252 | installed_games = cache.ListEntriesFilterOrigin( | ||
| 253 | ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program); | ||
| 254 | } | ||
| 238 | 255 | ||
| 239 | for (const auto& [slot, game] : installed_games) { | 256 | for (const auto& [slot, game] : installed_games) { |
| 240 | if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) | 257 | if (slot == ContentProviderUnionSlot::FrontendManual) |
| 241 | continue; | 258 | continue; |
| 242 | 259 | ||
| 243 | const auto file = cache.GetEntryUnparsed(game.title_id, game.type); | 260 | const auto file = cache.GetEntryUnparsed(game.title_id, game.type); |
| @@ -250,21 +267,22 @@ void GameListWorker::AddTitlesToGameList() { | |||
| 250 | u64 program_id = 0; | 267 | u64 program_id = 0; |
| 251 | loader->ReadProgramId(program_id); | 268 | loader->ReadProgramId(program_id); |
| 252 | 269 | ||
| 253 | const FileSys::PatchManager patch{program_id}; | 270 | const PatchManager patch{program_id}; |
| 254 | const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); | 271 | const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control); |
| 255 | if (control != nullptr) | 272 | if (control != nullptr) |
| 256 | GetMetadataFromControlNCA(patch, *control, icon, name); | 273 | GetMetadataFromControlNCA(patch, *control, icon, name); |
| 257 | 274 | ||
| 258 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, | 275 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, |
| 259 | compatibility_list, patch)); | 276 | compatibility_list, patch), |
| 277 | parent_dir); | ||
| 260 | } | 278 | } |
| 261 | } | 279 | } |
| 262 | 280 | ||
| 263 | void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, | 281 | void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, |
| 264 | unsigned int recursion) { | 282 | unsigned int recursion, GameListDir* parent_dir) { |
| 265 | const auto callback = [this, target, recursion](u64* num_entries_out, | 283 | const auto callback = [this, target, recursion, |
| 266 | const std::string& directory, | 284 | parent_dir](u64* num_entries_out, const std::string& directory, |
| 267 | const std::string& virtual_name) -> bool { | 285 | const std::string& virtual_name) -> bool { |
| 268 | if (stop_processing) { | 286 | if (stop_processing) { |
| 269 | // Breaks the callback loop. | 287 | // Breaks the callback loop. |
| 270 | return false; | 288 | return false; |
| @@ -317,11 +335,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 317 | const FileSys::PatchManager patch{program_id}; | 335 | const FileSys::PatchManager patch{program_id}; |
| 318 | 336 | ||
| 319 | emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, | 337 | emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, |
| 320 | compatibility_list, patch)); | 338 | compatibility_list, patch), |
| 339 | parent_dir); | ||
| 321 | } | 340 | } |
| 322 | } else if (is_dir && recursion > 0) { | 341 | } else if (is_dir && recursion > 0) { |
| 323 | watch_list.append(QString::fromStdString(physical_name)); | 342 | watch_list.append(QString::fromStdString(physical_name)); |
| 324 | ScanFileSystem(target, physical_name, recursion - 1); | 343 | ScanFileSystem(target, physical_name, recursion - 1, parent_dir); |
| 325 | } | 344 | } |
| 326 | 345 | ||
| 327 | return true; | 346 | return true; |
| @@ -332,12 +351,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 332 | 351 | ||
| 333 | void GameListWorker::run() { | 352 | void GameListWorker::run() { |
| 334 | stop_processing = false; | 353 | stop_processing = false; |
| 335 | watch_list.append(dir_path); | 354 | |
| 336 | provider->ClearAllEntries(); | 355 | for (UISettings::GameDir& game_dir : game_dirs) { |
| 337 | ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), | 356 | if (game_dir.path == "INSTALLED") { |
| 338 | deep_scan ? 256 : 0); | 357 | GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); |
| 339 | AddTitlesToGameList(); | 358 | emit DirEntryReady({game_list_dir}); |
| 340 | ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); | 359 | AddTitlesToGameList(game_list_dir); |
| 360 | } else if (game_dir.path == "SYSTEM") { | ||
| 361 | GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); | ||
| 362 | emit DirEntryReady({game_list_dir}); | ||
| 363 | AddTitlesToGameList(game_list_dir); | ||
| 364 | } else { | ||
| 365 | watch_list.append(game_dir.path); | ||
| 366 | GameListDir* game_list_dir = new GameListDir(game_dir); | ||
| 367 | emit DirEntryReady({game_list_dir}); | ||
| 368 | provider->ClearAllEntries(); | ||
| 369 | ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2, | ||
| 370 | game_list_dir); | ||
| 371 | ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(), | ||
| 372 | game_dir.deep_scan ? 256 : 0, game_list_dir); | ||
| 373 | } | ||
| 374 | }; | ||
| 375 | |||
| 341 | emit Finished(watch_list); | 376 | emit Finished(watch_list); |
| 342 | } | 377 | } |
| 343 | 378 | ||
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 7c3074af9..46ec96516 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h | |||
| @@ -33,9 +33,10 @@ class GameListWorker : public QObject, public QRunnable { | |||
| 33 | Q_OBJECT | 33 | Q_OBJECT |
| 34 | 34 | ||
| 35 | public: | 35 | public: |
| 36 | GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, | 36 | explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, |
| 37 | FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, | 37 | FileSys::ManualContentProvider* provider, |
| 38 | const CompatibilityList& compatibility_list); | 38 | QList<UISettings::GameDir>& game_dirs, |
| 39 | const CompatibilityList& compatibility_list); | ||
| 39 | ~GameListWorker() override; | 40 | ~GameListWorker() override; |
| 40 | 41 | ||
| 41 | /// Starts the processing of directory tree information. | 42 | /// Starts the processing of directory tree information. |
| @@ -48,31 +49,33 @@ signals: | |||
| 48 | /** | 49 | /** |
| 49 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready | 50 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready |
| 50 | * to be added to the game list. | 51 | * to be added to the game list. |
| 51 | * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. | 52 | * @param entry_items a list with `QStandardItem`s that make up the columns of the new |
| 53 | * entry. | ||
| 52 | */ | 54 | */ |
| 53 | void EntryReady(QList<QStandardItem*> entry_items); | 55 | void DirEntryReady(GameListDir* entry_items); |
| 56 | void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir); | ||
| 54 | 57 | ||
| 55 | /** | 58 | /** |
| 56 | * After the worker has traversed the game directory looking for entries, this signal is emitted | 59 | * After the worker has traversed the game directory looking for entries, this signal is |
| 57 | * with a list of folders that should be watched for changes as well. | 60 | * emitted with a list of folders that should be watched for changes as well. |
| 58 | */ | 61 | */ |
| 59 | void Finished(QStringList watch_list); | 62 | void Finished(QStringList watch_list); |
| 60 | 63 | ||
| 61 | private: | 64 | private: |
| 62 | void AddTitlesToGameList(); | 65 | void AddTitlesToGameList(GameListDir* parent_dir); |
| 63 | 66 | ||
| 64 | enum class ScanTarget { | 67 | enum class ScanTarget { |
| 65 | FillManualContentProvider, | 68 | FillManualContentProvider, |
| 66 | PopulateGameList, | 69 | PopulateGameList, |
| 67 | }; | 70 | }; |
| 68 | 71 | ||
| 69 | void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); | 72 | void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion, |
| 73 | GameListDir* parent_dir); | ||
| 70 | 74 | ||
| 71 | std::shared_ptr<FileSys::VfsFilesystem> vfs; | 75 | std::shared_ptr<FileSys::VfsFilesystem> vfs; |
| 72 | FileSys::ManualContentProvider* provider; | 76 | FileSys::ManualContentProvider* provider; |
| 73 | QStringList watch_list; | 77 | QStringList watch_list; |
| 74 | QString dir_path; | ||
| 75 | bool deep_scan; | ||
| 76 | const CompatibilityList& compatibility_list; | 78 | const CompatibilityList& compatibility_list; |
| 79 | QList<UISettings::GameDir>& game_dirs; | ||
| 77 | std::atomic_bool stop_processing; | 80 | std::atomic_bool stop_processing; |
| 78 | }; | 81 | }; |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ac57229d5..b2de9545b 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -216,8 +216,7 @@ GMainWindow::GMainWindow() | |||
| 216 | OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); | 216 | OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); |
| 217 | 217 | ||
| 218 | game_list->LoadCompatibilityList(); | 218 | game_list->LoadCompatibilityList(); |
| 219 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 219 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 220 | UISettings::values.game_directory_deepscan); | ||
| 221 | 220 | ||
| 222 | // Show one-time "callout" messages to the user | 221 | // Show one-time "callout" messages to the user |
| 223 | ShowTelemetryCallout(); | 222 | ShowTelemetryCallout(); |
| @@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() { | |||
| 427 | game_list = new GameList(vfs, provider.get(), this); | 426 | game_list = new GameList(vfs, provider.get(), this); |
| 428 | ui.horizontalLayout->addWidget(game_list); | 427 | ui.horizontalLayout->addWidget(game_list); |
| 429 | 428 | ||
| 429 | game_list_placeholder = new GameListPlaceholder(this); | ||
| 430 | ui.horizontalLayout->addWidget(game_list_placeholder); | ||
| 431 | game_list_placeholder->setVisible(false); | ||
| 432 | |||
| 430 | loading_screen = new LoadingScreen(this); | 433 | loading_screen = new LoadingScreen(this); |
| 431 | loading_screen->hide(); | 434 | loading_screen->hide(); |
| 432 | ui.horizontalLayout->addWidget(loading_screen); | 435 | ui.horizontalLayout->addWidget(loading_screen); |
| @@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() { | |||
| 660 | 663 | ||
| 661 | void GMainWindow::ConnectWidgetEvents() { | 664 | void GMainWindow::ConnectWidgetEvents() { |
| 662 | connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); | 665 | connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); |
| 666 | connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); | ||
| 663 | connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); | 667 | connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); |
| 664 | connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, | 668 | connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, |
| 665 | &GMainWindow::OnTransferableShaderCacheOpenFile); | 669 | &GMainWindow::OnTransferableShaderCacheOpenFile); |
| @@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() { | |||
| 667 | connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); | 671 | connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); |
| 668 | connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, | 672 | connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, |
| 669 | &GMainWindow::OnGameListNavigateToGamedbEntry); | 673 | &GMainWindow::OnGameListNavigateToGamedbEntry); |
| 674 | connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); | ||
| 675 | connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, | ||
| 676 | &GMainWindow::OnGameListAddDirectory); | ||
| 677 | connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); | ||
| 678 | |||
| 670 | connect(game_list, &GameList::OpenPerGameGeneralRequested, this, | 679 | connect(game_list, &GameList::OpenPerGameGeneralRequested, this, |
| 671 | &GMainWindow::OnGameListOpenPerGameProperties); | 680 | &GMainWindow::OnGameListOpenPerGameProperties); |
| 672 | 681 | ||
| @@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() { | |||
| 684 | connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); | 693 | connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); |
| 685 | connect(ui.action_Install_File_NAND, &QAction::triggered, this, | 694 | connect(ui.action_Install_File_NAND, &QAction::triggered, this, |
| 686 | &GMainWindow::OnMenuInstallToNAND); | 695 | &GMainWindow::OnMenuInstallToNAND); |
| 687 | connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, | ||
| 688 | &GMainWindow::OnMenuSelectGameListRoot); | ||
| 689 | connect(ui.action_Select_NAND_Directory, &QAction::triggered, this, | 696 | connect(ui.action_Select_NAND_Directory, &QAction::triggered, this, |
| 690 | [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); }); | 697 | [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); }); |
| 691 | connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, | 698 | connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, |
| @@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) { | |||
| 950 | // Update the GUI | 957 | // Update the GUI |
| 951 | if (ui.action_Single_Window_Mode->isChecked()) { | 958 | if (ui.action_Single_Window_Mode->isChecked()) { |
| 952 | game_list->hide(); | 959 | game_list->hide(); |
| 960 | game_list_placeholder->hide(); | ||
| 953 | } | 961 | } |
| 954 | status_bar_update_timer.start(2000); | 962 | status_bar_update_timer.start(2000); |
| 955 | 963 | ||
| @@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() { | |||
| 1007 | render_window->hide(); | 1015 | render_window->hide(); |
| 1008 | loading_screen->hide(); | 1016 | loading_screen->hide(); |
| 1009 | loading_screen->Clear(); | 1017 | loading_screen->Clear(); |
| 1010 | game_list->show(); | 1018 | if (game_list->isEmpty()) |
| 1019 | game_list_placeholder->show(); | ||
| 1020 | else | ||
| 1021 | game_list->show(); | ||
| 1011 | game_list->setFilterFocus(); | 1022 | game_list->setFilterFocus(); |
| 1012 | 1023 | ||
| 1013 | UpdateWindowTitle(); | 1024 | UpdateWindowTitle(); |
| @@ -1298,6 +1309,45 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, | |||
| 1298 | QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); | 1309 | QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); |
| 1299 | } | 1310 | } |
| 1300 | 1311 | ||
| 1312 | void GMainWindow::OnGameListOpenDirectory(QString directory) { | ||
| 1313 | QString path; | ||
| 1314 | if (directory == QStringLiteral("INSTALLED")) { | ||
| 1315 | // TODO: Find a better solution when installing files to the SD card gets implemented | ||
| 1316 | path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() + | ||
| 1317 | std::string("user/Contents/registered")); | ||
| 1318 | } else if (directory == QStringLiteral("SYSTEM")) { | ||
| 1319 | path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir).c_str() + | ||
| 1320 | std::string("system/Contents/registered")); | ||
| 1321 | } else { | ||
| 1322 | path = directory; | ||
| 1323 | } | ||
| 1324 | if (!QFileInfo::exists(path)) { | ||
| 1325 | QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); | ||
| 1326 | return; | ||
| 1327 | } | ||
| 1328 | QDesktopServices::openUrl(QUrl::fromLocalFile(path)); | ||
| 1329 | } | ||
| 1330 | |||
| 1331 | void GMainWindow::OnGameListAddDirectory() { | ||
| 1332 | QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||
| 1333 | if (dir_path.isEmpty()) | ||
| 1334 | return; | ||
| 1335 | UISettings::GameDir game_dir{dir_path, false, true}; | ||
| 1336 | if (!UISettings::values.game_dirs.contains(game_dir)) { | ||
| 1337 | UISettings::values.game_dirs.append(game_dir); | ||
| 1338 | game_list->PopulateAsync(UISettings::values.game_dirs); | ||
| 1339 | } else { | ||
| 1340 | LOG_WARNING(Frontend, "Selected directory is already in the game list"); | ||
| 1341 | } | ||
| 1342 | } | ||
| 1343 | |||
| 1344 | void GMainWindow::OnGameListShowList(bool show) { | ||
| 1345 | if (emulation_running && ui.action_Single_Window_Mode->isChecked()) | ||
| 1346 | return; | ||
| 1347 | game_list->setVisible(show); | ||
| 1348 | game_list_placeholder->setVisible(!show); | ||
| 1349 | }; | ||
| 1350 | |||
| 1301 | void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { | 1351 | void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { |
| 1302 | u64 title_id{}; | 1352 | u64 title_id{}; |
| 1303 | const auto v_file = Core::GetGameFileFromPath(vfs, file); | 1353 | const auto v_file = Core::GetGameFileFromPath(vfs, file); |
| @@ -1316,8 +1366,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { | |||
| 1316 | 1366 | ||
| 1317 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); | 1367 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); |
| 1318 | if (reload) { | 1368 | if (reload) { |
| 1319 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1369 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1320 | UISettings::values.game_directory_deepscan); | ||
| 1321 | } | 1370 | } |
| 1322 | 1371 | ||
| 1323 | config->Save(); | 1372 | config->Save(); |
| @@ -1407,8 +1456,7 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 1407 | const auto success = [this]() { | 1456 | const auto success = [this]() { |
| 1408 | QMessageBox::information(this, tr("Successfully Installed"), | 1457 | QMessageBox::information(this, tr("Successfully Installed"), |
| 1409 | tr("The file was successfully installed.")); | 1458 | tr("The file was successfully installed.")); |
| 1410 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1459 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1411 | UISettings::values.game_directory_deepscan); | ||
| 1412 | FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + | 1460 | FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + |
| 1413 | DIR_SEP + "game_list"); | 1461 | DIR_SEP + "game_list"); |
| 1414 | }; | 1462 | }; |
| @@ -1533,14 +1581,6 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 1533 | } | 1581 | } |
| 1534 | } | 1582 | } |
| 1535 | 1583 | ||
| 1536 | void GMainWindow::OnMenuSelectGameListRoot() { | ||
| 1537 | QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||
| 1538 | if (!dir_path.isEmpty()) { | ||
| 1539 | UISettings::values.game_directory_path = dir_path; | ||
| 1540 | game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan); | ||
| 1541 | } | ||
| 1542 | } | ||
| 1543 | |||
| 1544 | void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { | 1584 | void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { |
| 1545 | const auto res = QMessageBox::information( | 1585 | const auto res = QMessageBox::information( |
| 1546 | this, tr("Changing Emulated Directory"), | 1586 | this, tr("Changing Emulated Directory"), |
| @@ -1559,8 +1599,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) | |||
| 1559 | : FileUtil::UserPath::NANDDir, | 1599 | : FileUtil::UserPath::NANDDir, |
| 1560 | dir_path.toStdString()); | 1600 | dir_path.toStdString()); |
| 1561 | Service::FileSystem::CreateFactories(*vfs); | 1601 | Service::FileSystem::CreateFactories(*vfs); |
| 1562 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1602 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1563 | UISettings::values.game_directory_deepscan); | ||
| 1564 | } | 1603 | } |
| 1565 | } | 1604 | } |
| 1566 | 1605 | ||
| @@ -1724,11 +1763,11 @@ void GMainWindow::OnConfigure() { | |||
| 1724 | if (UISettings::values.enable_discord_presence != old_discord_presence) { | 1763 | if (UISettings::values.enable_discord_presence != old_discord_presence) { |
| 1725 | SetDiscordEnabled(UISettings::values.enable_discord_presence); | 1764 | SetDiscordEnabled(UISettings::values.enable_discord_presence); |
| 1726 | } | 1765 | } |
| 1766 | emit UpdateThemedIcons(); | ||
| 1727 | 1767 | ||
| 1728 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); | 1768 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); |
| 1729 | if (reload) { | 1769 | if (reload) { |
| 1730 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1770 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1731 | UISettings::values.game_directory_deepscan); | ||
| 1732 | } | 1771 | } |
| 1733 | 1772 | ||
| 1734 | config->Save(); | 1773 | config->Save(); |
| @@ -1992,8 +2031,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { | |||
| 1992 | Service::FileSystem::CreateFactories(*vfs); | 2031 | Service::FileSystem::CreateFactories(*vfs); |
| 1993 | 2032 | ||
| 1994 | if (behavior == ReinitializeKeyBehavior::Warning) { | 2033 | if (behavior == ReinitializeKeyBehavior::Warning) { |
| 1995 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 2034 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1996 | UISettings::values.game_directory_deepscan); | ||
| 1997 | } | 2035 | } |
| 1998 | } | 2036 | } |
| 1999 | 2037 | ||
| @@ -2158,7 +2196,6 @@ void GMainWindow::UpdateUITheme() { | |||
| 2158 | } | 2196 | } |
| 2159 | 2197 | ||
| 2160 | QIcon::setThemeSearchPaths(theme_paths); | 2198 | QIcon::setThemeSearchPaths(theme_paths); |
| 2161 | emit UpdateThemedIcons(); | ||
| 2162 | } | 2199 | } |
| 2163 | 2200 | ||
| 2164 | void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { | 2201 | void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 501608ddc..b7398b6c7 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -30,6 +30,7 @@ class ProfilerWidget; | |||
| 30 | class QLabel; | 30 | class QLabel; |
| 31 | class WaitTreeWidget; | 31 | class WaitTreeWidget; |
| 32 | enum class GameListOpenTarget; | 32 | enum class GameListOpenTarget; |
| 33 | class GameListPlaceholder; | ||
| 33 | 34 | ||
| 34 | namespace Core::Frontend { | 35 | namespace Core::Frontend { |
| 35 | struct SoftwareKeyboardParameters; | 36 | struct SoftwareKeyboardParameters; |
| @@ -186,12 +187,13 @@ private slots: | |||
| 186 | void OnGameListCopyTID(u64 program_id); | 187 | void OnGameListCopyTID(u64 program_id); |
| 187 | void OnGameListNavigateToGamedbEntry(u64 program_id, | 188 | void OnGameListNavigateToGamedbEntry(u64 program_id, |
| 188 | const CompatibilityList& compatibility_list); | 189 | const CompatibilityList& compatibility_list); |
| 190 | void OnGameListOpenDirectory(QString path); | ||
| 191 | void OnGameListAddDirectory(); | ||
| 192 | void OnGameListShowList(bool show); | ||
| 189 | void OnGameListOpenPerGameProperties(const std::string& file); | 193 | void OnGameListOpenPerGameProperties(const std::string& file); |
| 190 | void OnMenuLoadFile(); | 194 | void OnMenuLoadFile(); |
| 191 | void OnMenuLoadFolder(); | 195 | void OnMenuLoadFolder(); |
| 192 | void OnMenuInstallToNAND(); | 196 | void OnMenuInstallToNAND(); |
| 193 | /// Called whenever a user selects the "File->Select Game List Root" menu item | ||
| 194 | void OnMenuSelectGameListRoot(); | ||
| 195 | /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card | 197 | /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card |
| 196 | void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); | 198 | void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); |
| 197 | void OnMenuRecentFile(); | 199 | void OnMenuRecentFile(); |
| @@ -223,6 +225,8 @@ private: | |||
| 223 | GameList* game_list; | 225 | GameList* game_list; |
| 224 | LoadingScreen* loading_screen; | 226 | LoadingScreen* loading_screen; |
| 225 | 227 | ||
| 228 | GameListPlaceholder* game_list_placeholder; | ||
| 229 | |||
| 226 | // Status bar elements | 230 | // Status bar elements |
| 227 | QLabel* message_label = nullptr; | 231 | QLabel* message_label = nullptr; |
| 228 | QLabel* emu_speed_label = nullptr; | 232 | QLabel* emu_speed_label = nullptr; |
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index ffcabb495..a1ce3c0c3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui | |||
| @@ -62,7 +62,6 @@ | |||
| 62 | <addaction name="action_Load_File"/> | 62 | <addaction name="action_Load_File"/> |
| 63 | <addaction name="action_Load_Folder"/> | 63 | <addaction name="action_Load_Folder"/> |
| 64 | <addaction name="separator"/> | 64 | <addaction name="separator"/> |
| 65 | <addaction name="action_Select_Game_List_Root"/> | ||
| 66 | <addaction name="menu_recent_files"/> | 65 | <addaction name="menu_recent_files"/> |
| 67 | <addaction name="separator"/> | 66 | <addaction name="separator"/> |
| 68 | <addaction name="action_Select_NAND_Directory"/> | 67 | <addaction name="action_Select_NAND_Directory"/> |
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index a62cd6911..76348db69 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | #include <atomic> | 8 | #include <atomic> |
| 9 | #include <vector> | 9 | #include <vector> |
| 10 | #include <QByteArray> | 10 | #include <QByteArray> |
| 11 | #include <QMetaType> | ||
| 11 | #include <QString> | 12 | #include <QString> |
| 12 | #include <QStringList> | 13 | #include <QStringList> |
| 13 | #include "common/common_types.h" | 14 | #include "common/common_types.h" |
| @@ -25,6 +26,18 @@ struct Shortcut { | |||
| 25 | using Themes = std::array<std::pair<const char*, const char*>, 2>; | 26 | using Themes = std::array<std::pair<const char*, const char*>, 2>; |
| 26 | extern const Themes themes; | 27 | extern const Themes themes; |
| 27 | 28 | ||
| 29 | struct GameDir { | ||
| 30 | QString path; | ||
| 31 | bool deep_scan; | ||
| 32 | bool expanded; | ||
| 33 | bool operator==(const GameDir& rhs) const { | ||
| 34 | return path == rhs.path; | ||
| 35 | }; | ||
| 36 | bool operator!=(const GameDir& rhs) const { | ||
| 37 | return !operator==(rhs); | ||
| 38 | }; | ||
| 39 | }; | ||
| 40 | |||
| 28 | struct Values { | 41 | struct Values { |
| 29 | QByteArray geometry; | 42 | QByteArray geometry; |
| 30 | QByteArray state; | 43 | QByteArray state; |
| @@ -55,8 +68,9 @@ struct Values { | |||
| 55 | QString roms_path; | 68 | QString roms_path; |
| 56 | QString symbols_path; | 69 | QString symbols_path; |
| 57 | QString screenshot_path; | 70 | QString screenshot_path; |
| 58 | QString game_directory_path; | 71 | QString game_dir_deprecated; |
| 59 | bool game_directory_deepscan; | 72 | bool game_dir_deprecated_deepscan; |
| 73 | QList<UISettings::GameDir> game_dirs; | ||
| 60 | QStringList recent_files; | 74 | QStringList recent_files; |
| 61 | 75 | ||
| 62 | QString theme; | 76 | QString theme; |
| @@ -84,3 +98,5 @@ struct Values { | |||
| 84 | 98 | ||
| 85 | extern Values values; | 99 | extern Values values; |
| 86 | } // namespace UISettings | 100 | } // namespace UISettings |
| 101 | |||
| 102 | Q_DECLARE_METATYPE(UISettings::GameDir*); | ||