diff options
| author | 2019-09-04 11:40:35 -0400 | |
|---|---|---|
| committer | 2019-09-04 11:40:35 -0400 | |
| commit | a139fdf4ac0283a4a5decc7d8605caa374fb5af2 (patch) | |
| tree | 7fcf5450dab7cd70efddefe8729b8503d6552ad4 /src | |
| parent | Merge pull request #2835 from chris062689/master (diff) | |
| parent | Fix uisettings include (diff) | |
| download | yuzu-a139fdf4ac0283a4a5decc7d8605caa374fb5af2.tar.gz yuzu-a139fdf4ac0283a4a5decc7d8605caa374fb5af2.tar.xz yuzu-a139fdf4ac0283a4a5decc7d8605caa374fb5af2.zip | |
Merge pull request #2444 from FearlessTobi/port-3617-new
Port citra-emu/citra#3617: "QT: Add support for multiple game directories"
Diffstat (limited to 'src')
| -rw-r--r-- | src/yuzu/configuration/config.cpp | 44 | ||||
| -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 | 436 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 43 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 127 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.cpp | 86 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.h | 26 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 87 | ||||
| -rw-r--r-- | src/yuzu/main.h | 8 | ||||
| -rw-r--r-- | src/yuzu/main.ui | 1 | ||||
| -rw-r--r-- | src/yuzu/uisettings.h | 21 |
12 files changed, 695 insertions, 196 deletions
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 0456248ac..f594106bf 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp | |||
| @@ -517,10 +517,37 @@ 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 | const 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("SDMC"); | ||
| 539 | game_dir.expanded = true; | ||
| 540 | UISettings::values.game_dirs.append(game_dir); | ||
| 541 | game_dir.path = QStringLiteral("UserNAND"); | ||
| 542 | UISettings::values.game_dirs.append(game_dir); | ||
| 543 | game_dir.path = QStringLiteral("SysNAND"); | ||
| 544 | UISettings::values.game_dirs.append(game_dir); | ||
| 545 | if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) { | ||
| 546 | game_dir.path = UISettings::values.game_dir_deprecated; | ||
| 547 | game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan; | ||
| 548 | UISettings::values.game_dirs.append(game_dir); | ||
| 549 | } | ||
| 550 | } | ||
| 524 | UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); | 551 | UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); |
| 525 | 552 | ||
| 526 | qt_config->endGroup(); | 553 | qt_config->endGroup(); |
| @@ -899,10 +926,15 @@ void Config::SavePathValues() { | |||
| 899 | WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); | 926 | WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); |
| 900 | WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); | 927 | WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); |
| 901 | WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); | 928 | WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); |
| 902 | WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, | 929 | qt_config->beginWriteArray(QStringLiteral("gamedirs")); |
| 903 | QStringLiteral(".")); | 930 | for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { |
| 904 | WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, | 931 | qt_config->setArrayIndex(i); |
| 905 | false); | 932 | const auto& game_dir = UISettings::values.game_dirs[i]; |
| 933 | WriteSetting(QStringLiteral("path"), game_dir.path); | ||
| 934 | WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false); | ||
| 935 | WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); | ||
| 936 | } | ||
| 937 | qt_config->endArray(); | ||
| 906 | WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); | 938 | WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); |
| 907 | 939 | ||
| 908 | qt_config->endGroup(); | 940 | 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..d5fab2f1f 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() const { | ||
| 87 | QStandardItem* folder; | ||
| 88 | QStandardItem* child; | ||
| 89 | QString file_path; | ||
| 90 | const int folder_count = item_model->rowCount(); | ||
| 91 | for (int i = 0; i < folder_count; ++i) { | ||
| 92 | folder = item_model->item(i, 0); | ||
| 93 | const QModelIndex folder_index = folder->index(); | ||
| 94 | const int children_count = folder->rowCount(); | ||
| 95 | for (int j = 0; j < children_count; ++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,120 @@ 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 | const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>(); | ||
| 164 | if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir || | ||
| 165 | type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir) | ||
| 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 | const 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 children_total = 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 | const QModelIndex folder_index = folder->index(); | ||
| 185 | const int children_count = folder->rowCount(); | ||
| 186 | for (int j = 0; j < children_count; ++j) { | ||
| 187 | ++children_total; | ||
| 188 | tree_view->setRowHidden(j, folder_index, false); | ||
| 189 | } | ||
| 161 | } | 190 | } |
| 162 | search_field->setFilterResult(row_count, row_count); | 191 | search_field->setFilterResult(children_total, children_total); |
| 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 | const QModelIndex folder_index = folder->index(); |
| 168 | child_file->data(GameListItemPath::FullPathRole).toString().toLower(); | 197 | const int children_count = folder->rowCount(); |
| 169 | const QString file_title = | 198 | for (int j = 0; j < children_count; ++j) { |
| 170 | child_file->data(GameListItemPath::TitleRole).toString().toLower(); | 199 | ++children_total; |
| 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, children_total); | ||
| 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 | const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); | ||
| 234 | switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) { | ||
| 235 | case GameListItemType::SdmcDir: | ||
| 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::UserNandDir: | ||
| 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::SysNandDir: | ||
| 250 | child->setData( | ||
| 251 | QIcon::fromTheme(QStringLiteral("chip")) | ||
| 252 | .pixmap(icon_size) | ||
| 253 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 254 | Qt::DecorationRole); | ||
| 255 | break; | ||
| 256 | case GameListItemType::CustomDir: { | ||
| 257 | const UISettings::GameDir* game_dir = | ||
| 258 | child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 259 | const QString icon_name = QFileInfo::exists(game_dir->path) | ||
| 260 | ? QStringLiteral("folder") | ||
| 261 | : QStringLiteral("bad_folder"); | ||
| 262 | child->setData( | ||
| 263 | QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( | ||
| 264 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 265 | Qt::DecorationRole); | ||
| 266 | break; | ||
| 267 | } | ||
| 268 | case GameListItemType::AddDir: | ||
| 269 | child->setData( | ||
| 270 | QIcon::fromTheme(QStringLiteral("plus")) | ||
| 271 | .pixmap(icon_size) | ||
| 272 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 273 | Qt::DecorationRole); | ||
| 274 | break; | ||
| 189 | } | 275 | } |
| 190 | } | 276 | } |
| 191 | } | 277 | } |
| @@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide | |||
| 214 | tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); | 300 | tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); |
| 215 | tree_view->setSortingEnabled(true); | 301 | tree_view->setSortingEnabled(true); |
| 216 | tree_view->setEditTriggers(QHeaderView::NoEditTriggers); | 302 | tree_view->setEditTriggers(QHeaderView::NoEditTriggers); |
| 217 | tree_view->setUniformRowHeights(true); | ||
| 218 | tree_view->setContextMenuPolicy(Qt::CustomContextMenu); | 303 | tree_view->setContextMenuPolicy(Qt::CustomContextMenu); |
| 219 | tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); | 304 | tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); |
| 220 | 305 | ||
| @@ -230,12 +315,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide | |||
| 230 | item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); | 315 | item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); |
| 231 | item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); | 316 | item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); |
| 232 | } | 317 | } |
| 318 | item_model->setSortRole(GameListItemPath::TitleRole); | ||
| 233 | 319 | ||
| 320 | connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons); | ||
| 234 | connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); | 321 | connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); |
| 235 | connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); | 322 | connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); |
| 323 | connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded); | ||
| 324 | connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded); | ||
| 236 | 325 | ||
| 237 | // We must register all custom types with the Qt Automoc system so that we are able to use it | 326 | // 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. | 327 | // it with signals/slots. In this case, QList falls under the umbrells of custom types. |
| 239 | qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); | 328 | qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); |
| 240 | 329 | ||
| 241 | layout->setContentsMargins(0, 0, 0, 0); | 330 | layout->setContentsMargins(0, 0, 0, 0); |
| @@ -263,38 +352,68 @@ void GameList::clearFilter() { | |||
| 263 | search_field->clear(); | 352 | search_field->clear(); |
| 264 | } | 353 | } |
| 265 | 354 | ||
| 266 | void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { | 355 | void GameList::AddDirEntry(GameListDir* entry_items) { |
| 267 | item_model->invisibleRootItem()->appendRow(entry_items); | 356 | item_model->invisibleRootItem()->appendRow(entry_items); |
| 357 | tree_view->setExpanded( | ||
| 358 | entry_items->index(), | ||
| 359 | entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded); | ||
| 268 | } | 360 | } |
| 269 | 361 | ||
| 270 | void GameList::ValidateEntry(const QModelIndex& item) { | 362 | 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. | 363 | parent->appendRow(entry_items); |
| 272 | const int row = item_model->itemFromIndex(item)->row(); | 364 | } |
| 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 | 365 | ||
| 282 | const QFileInfo file_info{file_path}; | 366 | void GameList::ValidateEntry(const QModelIndex& item) { |
| 283 | if (file_info.isDir()) { | 367 | const auto selected = item.sibling(item.row(), 0); |
| 284 | const QDir dir{file_path}; | 368 | |
| 285 | const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); | 369 | switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { |
| 286 | if (matching_main.size() == 1) { | 370 | case GameListItemType::Game: { |
| 287 | emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); | 371 | const QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); |
| 372 | if (file_path.isEmpty()) | ||
| 373 | return; | ||
| 374 | const QFileInfo file_info(file_path); | ||
| 375 | if (!file_info.exists()) | ||
| 376 | return; | ||
| 377 | |||
| 378 | if (file_info.isDir()) { | ||
| 379 | const QDir dir{file_path}; | ||
| 380 | const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); | ||
| 381 | if (matching_main.size() == 1) { | ||
| 382 | emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); | ||
| 383 | } | ||
| 384 | return; | ||
| 288 | } | 385 | } |
| 289 | return; | 386 | |
| 387 | // Users usually want to run a different game after closing one | ||
| 388 | search_field->clear(); | ||
| 389 | emit GameChosen(file_path); | ||
| 390 | break; | ||
| 290 | } | 391 | } |
| 392 | case GameListItemType::AddDir: | ||
| 393 | emit AddDirectory(); | ||
| 394 | break; | ||
| 395 | } | ||
| 396 | } | ||
| 291 | 397 | ||
| 292 | // Users usually want to run a diffrent game after closing one | 398 | bool GameList::isEmpty() const { |
| 293 | search_field->clear(); | 399 | for (int i = 0; i < item_model->rowCount(); i++) { |
| 294 | emit GameChosen(file_path); | 400 | const QStandardItem* child = item_model->invisibleRootItem()->child(i); |
| 401 | const auto type = static_cast<GameListItemType>(child->type()); | ||
| 402 | if (!child->hasChildren() && | ||
| 403 | (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || | ||
| 404 | type == GameListItemType::SysNandDir)) { | ||
| 405 | item_model->invisibleRootItem()->removeRow(child->row()); | ||
| 406 | i--; | ||
| 407 | }; | ||
| 408 | } | ||
| 409 | return !item_model->invisibleRootItem()->hasChildren(); | ||
| 295 | } | 410 | } |
| 296 | 411 | ||
| 297 | void GameList::DonePopulating(QStringList watch_list) { | 412 | void GameList::DonePopulating(QStringList watch_list) { |
| 413 | emit ShowList(!isEmpty()); | ||
| 414 | |||
| 415 | item_model->invisibleRootItem()->appendRow(new GameListAddDir()); | ||
| 416 | |||
| 298 | // Clear out the old directories to watch for changes and add the new ones | 417 | // Clear out the old directories to watch for changes and add the new ones |
| 299 | auto watch_dirs = watcher->directories(); | 418 | auto watch_dirs = watcher->directories(); |
| 300 | if (!watch_dirs.isEmpty()) { | 419 | if (!watch_dirs.isEmpty()) { |
| @@ -311,9 +430,13 @@ void GameList::DonePopulating(QStringList watch_list) { | |||
| 311 | QCoreApplication::processEvents(); | 430 | QCoreApplication::processEvents(); |
| 312 | } | 431 | } |
| 313 | tree_view->setEnabled(true); | 432 | tree_view->setEnabled(true); |
| 314 | int rowCount = tree_view->model()->rowCount(); | 433 | const int folder_count = tree_view->model()->rowCount(); |
| 315 | search_field->setFilterResult(rowCount, rowCount); | 434 | int children_total = 0; |
| 316 | if (rowCount > 0) { | 435 | for (int i = 0; i < folder_count; ++i) { |
| 436 | children_total += item_model->item(i, 0)->rowCount(); | ||
| 437 | } | ||
| 438 | search_field->setFilterResult(children_total, children_total); | ||
| 439 | if (children_total > 0) { | ||
| 317 | search_field->setFocus(); | 440 | search_field->setFocus(); |
| 318 | } | 441 | } |
| 319 | } | 442 | } |
| @@ -323,12 +446,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
| 323 | if (!item.isValid()) | 446 | if (!item.isValid()) |
| 324 | return; | 447 | return; |
| 325 | 448 | ||
| 326 | int row = item_model->itemFromIndex(item)->row(); | 449 | const 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; | 450 | QMenu context_menu; |
| 451 | switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||
| 452 | case GameListItemType::Game: | ||
| 453 | AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(), | ||
| 454 | selected.data(GameListItemPath::FullPathRole).toString().toStdString()); | ||
| 455 | break; | ||
| 456 | case GameListItemType::CustomDir: | ||
| 457 | AddPermDirPopup(context_menu, selected); | ||
| 458 | AddCustomDirPopup(context_menu, selected); | ||
| 459 | break; | ||
| 460 | case GameListItemType::SdmcDir: | ||
| 461 | case GameListItemType::UserNandDir: | ||
| 462 | case GameListItemType::SysNandDir: | ||
| 463 | AddPermDirPopup(context_menu, selected); | ||
| 464 | break; | ||
| 465 | } | ||
| 466 | context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | ||
| 467 | } | ||
| 468 | |||
| 469 | 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")); | 470 | 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")); | 471 | QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); |
| 334 | QAction* open_transferable_shader_cache = | 472 | QAction* open_transferable_shader_cache = |
| @@ -344,19 +482,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
| 344 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | 482 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); |
| 345 | navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); | 483 | navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); |
| 346 | 484 | ||
| 347 | connect(open_save_location, &QAction::triggered, | 485 | connect(open_save_location, &QAction::triggered, [this, program_id]() { |
| 348 | [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); | 486 | emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); |
| 349 | connect(open_lfs_location, &QAction::triggered, | 487 | }); |
| 350 | [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); | 488 | connect(open_lfs_location, &QAction::triggered, [this, program_id]() { |
| 489 | emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); | ||
| 490 | }); | ||
| 351 | connect(open_transferable_shader_cache, &QAction::triggered, | 491 | connect(open_transferable_shader_cache, &QAction::triggered, |
| 352 | [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); | 492 | [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); |
| 353 | connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); | 493 | connect(dump_romfs, &QAction::triggered, |
| 354 | connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); | 494 | [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); }); |
| 355 | connect(navigate_to_gamedb_entry, &QAction::triggered, | 495 | connect(copy_tid, &QAction::triggered, |
| 356 | [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); | 496 | [this, program_id]() { emit CopyTIDRequested(program_id); }); |
| 357 | connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); | 497 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { |
| 498 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); | ||
| 499 | }); | ||
| 500 | connect(properties, &QAction::triggered, | ||
| 501 | [this, path]() { emit OpenPerGameGeneralRequested(path); }); | ||
| 502 | }; | ||
| 503 | |||
| 504 | void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { | ||
| 505 | UISettings::GameDir& game_dir = | ||
| 506 | *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 507 | |||
| 508 | QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); | ||
| 509 | QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); | ||
| 510 | |||
| 511 | deep_scan->setCheckable(true); | ||
| 512 | deep_scan->setChecked(game_dir.deep_scan); | ||
| 513 | |||
| 514 | connect(deep_scan, &QAction::triggered, [this, &game_dir] { | ||
| 515 | game_dir.deep_scan = !game_dir.deep_scan; | ||
| 516 | PopulateAsync(UISettings::values.game_dirs); | ||
| 517 | }); | ||
| 518 | connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { | ||
| 519 | UISettings::values.game_dirs.removeOne(game_dir); | ||
| 520 | item_model->invisibleRootItem()->removeRow(selected.row()); | ||
| 521 | }); | ||
| 522 | } | ||
| 358 | 523 | ||
| 359 | context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | 524 | void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { |
| 525 | UISettings::GameDir& game_dir = | ||
| 526 | *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); | ||
| 527 | |||
| 528 | QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up")); | ||
| 529 | QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down ")); | ||
| 530 | QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location")); | ||
| 531 | |||
| 532 | const int row = selected.row(); | ||
| 533 | |||
| 534 | move_up->setEnabled(row > 0); | ||
| 535 | move_down->setEnabled(row < item_model->rowCount() - 2); | ||
| 536 | |||
| 537 | connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] { | ||
| 538 | // find the indices of the items in settings and swap them | ||
| 539 | std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], | ||
| 540 | UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( | ||
| 541 | *selected.sibling(row - 1, 0) | ||
| 542 | .data(GameListDir::GameDirRole) | ||
| 543 | .value<UISettings::GameDir*>())]); | ||
| 544 | // move the treeview items | ||
| 545 | QList<QStandardItem*> item = item_model->takeRow(row); | ||
| 546 | item_model->invisibleRootItem()->insertRow(row - 1, item); | ||
| 547 | tree_view->setExpanded(selected, game_dir.expanded); | ||
| 548 | }); | ||
| 549 | |||
| 550 | connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] { | ||
| 551 | // find the indices of the items in settings and swap them | ||
| 552 | std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], | ||
| 553 | UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( | ||
| 554 | *selected.sibling(row + 1, 0) | ||
| 555 | .data(GameListDir::GameDirRole) | ||
| 556 | .value<UISettings::GameDir*>())]); | ||
| 557 | // move the treeview items | ||
| 558 | const QList<QStandardItem*> item = item_model->takeRow(row); | ||
| 559 | item_model->invisibleRootItem()->insertRow(row + 1, item); | ||
| 560 | tree_view->setExpanded(selected, game_dir.expanded); | ||
| 561 | }); | ||
| 562 | |||
| 563 | connect(open_directory_location, &QAction::triggered, | ||
| 564 | [this, game_dir] { emit OpenDirectory(game_dir.path); }); | ||
| 360 | } | 565 | } |
| 361 | 566 | ||
| 362 | void GameList::LoadCompatibilityList() { | 567 | void GameList::LoadCompatibilityList() { |
| @@ -403,14 +608,7 @@ void GameList::LoadCompatibilityList() { | |||
| 403 | } | 608 | } |
| 404 | } | 609 | } |
| 405 | 610 | ||
| 406 | void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | 611 | void GameList::PopulateAsync(QVector<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); | 612 | tree_view->setEnabled(false); |
| 415 | 613 | ||
| 416 | // Update the columns in case UISettings has changed | 614 | // Update the columns in case UISettings has changed |
| @@ -433,17 +631,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { | |||
| 433 | 631 | ||
| 434 | // Delete any rows that might already exist if we're repopulating | 632 | // Delete any rows that might already exist if we're repopulating |
| 435 | item_model->removeRows(0, item_model->rowCount()); | 633 | item_model->removeRows(0, item_model->rowCount()); |
| 634 | search_field->clear(); | ||
| 436 | 635 | ||
| 437 | emit ShouldCancelWorker(); | 636 | emit ShouldCancelWorker(); |
| 438 | 637 | ||
| 439 | GameListWorker* worker = | 638 | GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list); |
| 440 | new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list); | ||
| 441 | 639 | ||
| 442 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); | 640 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); |
| 641 | connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, | ||
| 642 | Qt::QueuedConnection); | ||
| 443 | connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, | 643 | connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, |
| 444 | Qt::QueuedConnection); | 644 | Qt::QueuedConnection); |
| 445 | // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel | 645 | // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to |
| 446 | // without delay. | 646 | // cancel without delay. |
| 447 | connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, | 647 | connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, |
| 448 | Qt::DirectConnection); | 648 | Qt::DirectConnection); |
| 449 | 649 | ||
| @@ -471,10 +671,40 @@ const QStringList GameList::supported_file_extensions = { | |||
| 471 | QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; | 671 | QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; |
| 472 | 672 | ||
| 473 | void GameList::RefreshGameDirectory() { | 673 | void GameList::RefreshGameDirectory() { |
| 474 | if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { | 674 | if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { |
| 475 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); | 675 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); |
| 476 | search_field->clear(); | 676 | PopulateAsync(UISettings::values.game_dirs); |
| 477 | PopulateAsync(UISettings::values.game_directory_path, | ||
| 478 | UISettings::values.game_directory_deepscan); | ||
| 479 | } | 677 | } |
| 480 | } | 678 | } |
| 679 | |||
| 680 | GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { | ||
| 681 | connect(parent, &GMainWindow::UpdateThemedIcons, this, | ||
| 682 | &GameListPlaceholder::onUpdateThemedIcons); | ||
| 683 | |||
| 684 | layout = new QVBoxLayout; | ||
| 685 | image = new QLabel; | ||
| 686 | text = new QLabel; | ||
| 687 | layout->setAlignment(Qt::AlignCenter); | ||
| 688 | image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); | ||
| 689 | |||
| 690 | text->setText(tr("Double-click to add a new folder to the game list")); | ||
| 691 | QFont font = text->font(); | ||
| 692 | font.setPointSize(20); | ||
| 693 | text->setFont(font); | ||
| 694 | text->setAlignment(Qt::AlignHCenter); | ||
| 695 | image->setAlignment(Qt::AlignHCenter); | ||
| 696 | |||
| 697 | layout->addWidget(image); | ||
| 698 | layout->addWidget(text); | ||
| 699 | setLayout(layout); | ||
| 700 | } | ||
| 701 | |||
| 702 | GameListPlaceholder::~GameListPlaceholder() = default; | ||
| 703 | |||
| 704 | void GameListPlaceholder::onUpdateThemedIcons() { | ||
| 705 | image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); | ||
| 706 | } | ||
| 707 | |||
| 708 | void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { | ||
| 709 | emit GameListPlaceholder::AddDirectory(); | ||
| 710 | } | ||
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index f8f8bd6c5..878d94413 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | #include <QHBoxLayout> | 8 | #include <QHBoxLayout> |
| 9 | #include <QLabel> | 9 | #include <QLabel> |
| 10 | #include <QLineEdit> | 10 | #include <QLineEdit> |
| 11 | #include <QList> | ||
| 11 | #include <QModelIndex> | 12 | #include <QModelIndex> |
| 12 | #include <QSettings> | 13 | #include <QSettings> |
| 13 | #include <QStandardItem> | 14 | #include <QStandardItem> |
| @@ -16,13 +17,16 @@ | |||
| 16 | #include <QToolButton> | 17 | #include <QToolButton> |
| 17 | #include <QTreeView> | 18 | #include <QTreeView> |
| 18 | #include <QVBoxLayout> | 19 | #include <QVBoxLayout> |
| 20 | #include <QVector> | ||
| 19 | #include <QWidget> | 21 | #include <QWidget> |
| 20 | 22 | ||
| 21 | #include "common/common_types.h" | 23 | #include "common/common_types.h" |
| 24 | #include "uisettings.h" | ||
| 22 | #include "yuzu/compatibility_list.h" | 25 | #include "yuzu/compatibility_list.h" |
| 23 | 26 | ||
| 24 | class GameListWorker; | 27 | class GameListWorker; |
| 25 | class GameListSearchField; | 28 | class GameListSearchField; |
| 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() const; | ||
| 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() const; | ||
| 58 | 64 | ||
| 59 | void LoadCompatibilityList(); | 65 | void LoadCompatibilityList(); |
| 60 | void PopulateAsync(const QString& dir_path, bool deep_scan); | 66 | void PopulateAsync(QVector<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(const 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,24 @@ 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 | QVBoxLayout* layout = nullptr; | ||
| 139 | QLabel* image = nullptr; | ||
| 140 | QLabel* text = nullptr; | ||
| 141 | }; | ||
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index ece534dd6..a8d888fee 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,17 @@ | |||
| 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 | SdmcDir = QStandardItem::UserType + 3, | ||
| 30 | UserNandDir = QStandardItem::UserType + 4, | ||
| 31 | SysNandDir = QStandardItem::UserType + 5, | ||
| 32 | AddDir = QStandardItem::UserType + 6 | ||
| 33 | }; | ||
| 34 | |||
| 35 | Q_DECLARE_METATYPE(GameListItemType); | ||
| 36 | |||
| 25 | /** | 37 | /** |
| 26 | * Gets the default icon (for games without valid title metadata) | 38 | * Gets the default icon (for games without valid title metadata) |
| 27 | * @param size The desired width and height of the default icon. | 39 | * @param size The desired width and height of the default icon. |
| @@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) { | |||
| 36 | class GameListItem : public QStandardItem { | 48 | class GameListItem : public QStandardItem { |
| 37 | 49 | ||
| 38 | public: | 50 | public: |
| 51 | // used to access type from item index | ||
| 52 | static const int TypeRole = Qt::UserRole + 1; | ||
| 53 | static const int SortRole = Qt::UserRole + 2; | ||
| 39 | GameListItem() = default; | 54 | GameListItem() = default; |
| 40 | explicit GameListItem(const QString& string) : QStandardItem(string) {} | 55 | GameListItem(const QString& string) : QStandardItem(string) { |
| 56 | setData(string, SortRole); | ||
| 57 | } | ||
| 41 | }; | 58 | }; |
| 42 | 59 | ||
| 43 | /** | 60 | /** |
| @@ -48,14 +65,15 @@ public: | |||
| 48 | */ | 65 | */ |
| 49 | class GameListItemPath : public GameListItem { | 66 | class GameListItemPath : public GameListItem { |
| 50 | public: | 67 | public: |
| 51 | static const int FullPathRole = Qt::UserRole + 1; | 68 | static const int TitleRole = SortRole; |
| 52 | static const int TitleRole = Qt::UserRole + 2; | 69 | static const int FullPathRole = SortRole + 1; |
| 53 | static const int ProgramIdRole = Qt::UserRole + 3; | 70 | static const int ProgramIdRole = SortRole + 2; |
| 54 | static const int FileTypeRole = Qt::UserRole + 4; | 71 | static const int FileTypeRole = SortRole + 3; |
| 55 | 72 | ||
| 56 | GameListItemPath() = default; | 73 | GameListItemPath() = default; |
| 57 | GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, | 74 | GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, |
| 58 | const QString& game_name, const QString& game_type, u64 program_id) { | 75 | const QString& game_name, const QString& game_type, u64 program_id) { |
| 76 | setData(type(), TypeRole); | ||
| 59 | setData(game_path, FullPathRole); | 77 | setData(game_path, FullPathRole); |
| 60 | setData(game_name, TitleRole); | 78 | setData(game_name, TitleRole); |
| 61 | setData(qulonglong(program_id), ProgramIdRole); | 79 | setData(qulonglong(program_id), ProgramIdRole); |
| @@ -72,6 +90,10 @@ public: | |||
| 72 | setData(picture, Qt::DecorationRole); | 90 | setData(picture, Qt::DecorationRole); |
| 73 | } | 91 | } |
| 74 | 92 | ||
| 93 | int type() const override { | ||
| 94 | return static_cast<int>(GameListItemType::Game); | ||
| 95 | } | ||
| 96 | |||
| 75 | QVariant data(int role) const override { | 97 | QVariant data(int role) const override { |
| 76 | if (role == Qt::DisplayRole) { | 98 | if (role == Qt::DisplayRole) { |
| 77 | std::string filename; | 99 | std::string filename; |
| @@ -103,9 +125,11 @@ public: | |||
| 103 | class GameListItemCompat : public GameListItem { | 125 | class GameListItemCompat : public GameListItem { |
| 104 | Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) | 126 | Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) |
| 105 | public: | 127 | public: |
| 106 | static const int CompatNumberRole = Qt::UserRole + 1; | 128 | static const int CompatNumberRole = SortRole; |
| 107 | GameListItemCompat() = default; | 129 | GameListItemCompat() = default; |
| 108 | explicit GameListItemCompat(const QString& compatibility) { | 130 | explicit GameListItemCompat(const QString& compatibility) { |
| 131 | setData(type(), TypeRole); | ||
| 132 | |||
| 109 | struct CompatStatus { | 133 | struct CompatStatus { |
| 110 | QString color; | 134 | QString color; |
| 111 | const char* text; | 135 | const char* text; |
| @@ -135,6 +159,10 @@ public: | |||
| 135 | setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); | 159 | setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); |
| 136 | } | 160 | } |
| 137 | 161 | ||
| 162 | int type() const override { | ||
| 163 | return static_cast<int>(GameListItemType::Game); | ||
| 164 | } | ||
| 165 | |||
| 138 | bool operator<(const QStandardItem& other) const override { | 166 | bool operator<(const QStandardItem& other) const override { |
| 139 | return data(CompatNumberRole) < other.data(CompatNumberRole); | 167 | return data(CompatNumberRole) < other.data(CompatNumberRole); |
| 140 | } | 168 | } |
| @@ -146,12 +174,12 @@ public: | |||
| 146 | * human-readable string representation will be displayed to the user. | 174 | * human-readable string representation will be displayed to the user. |
| 147 | */ | 175 | */ |
| 148 | class GameListItemSize : public GameListItem { | 176 | class GameListItemSize : public GameListItem { |
| 149 | |||
| 150 | public: | 177 | public: |
| 151 | static const int SizeRole = Qt::UserRole + 1; | 178 | static const int SizeRole = SortRole; |
| 152 | 179 | ||
| 153 | GameListItemSize() = default; | 180 | GameListItemSize() = default; |
| 154 | explicit GameListItemSize(const qulonglong size_bytes) { | 181 | explicit GameListItemSize(const qulonglong size_bytes) { |
| 182 | setData(type(), TypeRole); | ||
| 155 | setData(size_bytes, SizeRole); | 183 | setData(size_bytes, SizeRole); |
| 156 | } | 184 | } |
| 157 | 185 | ||
| @@ -167,6 +195,10 @@ public: | |||
| 167 | } | 195 | } |
| 168 | } | 196 | } |
| 169 | 197 | ||
| 198 | int type() const override { | ||
| 199 | return static_cast<int>(GameListItemType::Game); | ||
| 200 | } | ||
| 201 | |||
| 170 | /** | 202 | /** |
| 171 | * This operator is, in practice, only used by the TreeView sorting systems. | 203 | * 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 | 204 | * Override it so that it will correctly sort by numerical value instead of by string |
| @@ -177,6 +209,82 @@ public: | |||
| 177 | } | 209 | } |
| 178 | }; | 210 | }; |
| 179 | 211 | ||
| 212 | class GameListDir : public GameListItem { | ||
| 213 | public: | ||
| 214 | static const int GameDirRole = Qt::UserRole + 2; | ||
| 215 | |||
| 216 | explicit GameListDir(UISettings::GameDir& directory, | ||
| 217 | GameListItemType dir_type = GameListItemType::CustomDir) | ||
| 218 | : dir_type{dir_type} { | ||
| 219 | setData(type(), TypeRole); | ||
| 220 | |||
| 221 | UISettings::GameDir* game_dir = &directory; | ||
| 222 | setData(QVariant::fromValue(game_dir), GameDirRole); | ||
| 223 | |||
| 224 | const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); | ||
| 225 | switch (dir_type) { | ||
| 226 | case GameListItemType::SdmcDir: | ||
| 227 | setData( | ||
| 228 | QIcon::fromTheme(QStringLiteral("sd_card")) | ||
| 229 | .pixmap(icon_size) | ||
| 230 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 231 | Qt::DecorationRole); | ||
| 232 | setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole); | ||
| 233 | break; | ||
| 234 | case GameListItemType::UserNandDir: | ||
| 235 | setData( | ||
| 236 | QIcon::fromTheme(QStringLiteral("chip")) | ||
| 237 | .pixmap(icon_size) | ||
| 238 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 239 | Qt::DecorationRole); | ||
| 240 | setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole); | ||
| 241 | break; | ||
| 242 | case GameListItemType::SysNandDir: | ||
| 243 | setData( | ||
| 244 | QIcon::fromTheme(QStringLiteral("chip")) | ||
| 245 | .pixmap(icon_size) | ||
| 246 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 247 | Qt::DecorationRole); | ||
| 248 | setData(QObject::tr("System Titles"), Qt::DisplayRole); | ||
| 249 | break; | ||
| 250 | case GameListItemType::CustomDir: | ||
| 251 | const QString icon_name = QFileInfo::exists(game_dir->path) | ||
| 252 | ? QStringLiteral("folder") | ||
| 253 | : QStringLiteral("bad_folder"); | ||
| 254 | setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( | ||
| 255 | icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 256 | Qt::DecorationRole); | ||
| 257 | setData(game_dir->path, Qt::DisplayRole); | ||
| 258 | break; | ||
| 259 | }; | ||
| 260 | }; | ||
| 261 | |||
| 262 | int type() const override { | ||
| 263 | return static_cast<int>(dir_type); | ||
| 264 | } | ||
| 265 | |||
| 266 | private: | ||
| 267 | GameListItemType dir_type; | ||
| 268 | }; | ||
| 269 | |||
| 270 | class GameListAddDir : public GameListItem { | ||
| 271 | public: | ||
| 272 | explicit GameListAddDir() { | ||
| 273 | setData(type(), TypeRole); | ||
| 274 | |||
| 275 | const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); | ||
| 276 | setData(QIcon::fromTheme(QStringLiteral("plus")) | ||
| 277 | .pixmap(icon_size) | ||
| 278 | .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), | ||
| 279 | Qt::DecorationRole); | ||
| 280 | setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole); | ||
| 281 | } | ||
| 282 | |||
| 283 | int type() const override { | ||
| 284 | return static_cast<int>(GameListItemType::AddDir); | ||
| 285 | } | ||
| 286 | }; | ||
| 287 | |||
| 180 | class GameList; | 288 | class GameList; |
| 181 | class QHBoxLayout; | 289 | class QHBoxLayout; |
| 182 | class QTreeView; | 290 | class QTreeView; |
| @@ -208,6 +316,9 @@ private: | |||
| 208 | // EventFilter in order to process systemkeys while editing the searchfield | 316 | // EventFilter in order to process systemkeys while editing the searchfield |
| 209 | bool eventFilter(QObject* obj, QEvent* event) override; | 317 | bool eventFilter(QObject* obj, QEvent* event) override; |
| 210 | }; | 318 | }; |
| 319 | int visible; | ||
| 320 | int total; | ||
| 321 | |||
| 211 | QHBoxLayout* layout_filter = nullptr; | 322 | QHBoxLayout* layout_filter = nullptr; |
| 212 | QTreeView* tree_view = nullptr; | 323 | QTreeView* tree_view = nullptr; |
| 213 | QLabel* label_filter = nullptr; | 324 | QLabel* label_filter = nullptr; |
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 77f358630..fd21a9761 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp | |||
| @@ -223,21 +223,37 @@ 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 | QVector<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 | |||
| 244 | if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) { | ||
| 245 | installed_games = cache.ListEntriesFilterOrigin( | ||
| 246 | ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program); | ||
| 247 | } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) { | ||
| 248 | installed_games = cache.ListEntriesFilterOrigin( | ||
| 249 | ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program); | ||
| 250 | } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) { | ||
| 251 | installed_games = cache.ListEntriesFilterOrigin( | ||
| 252 | ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program); | ||
| 253 | } | ||
| 238 | 254 | ||
| 239 | for (const auto& [slot, game] : installed_games) { | 255 | for (const auto& [slot, game] : installed_games) { |
| 240 | if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) | 256 | if (slot == ContentProviderUnionSlot::FrontendManual) |
| 241 | continue; | 257 | continue; |
| 242 | 258 | ||
| 243 | const auto file = cache.GetEntryUnparsed(game.title_id, game.type); | 259 | const auto file = cache.GetEntryUnparsed(game.title_id, game.type); |
| @@ -250,21 +266,22 @@ void GameListWorker::AddTitlesToGameList() { | |||
| 250 | u64 program_id = 0; | 266 | u64 program_id = 0; |
| 251 | loader->ReadProgramId(program_id); | 267 | loader->ReadProgramId(program_id); |
| 252 | 268 | ||
| 253 | const FileSys::PatchManager patch{program_id}; | 269 | const PatchManager patch{program_id}; |
| 254 | const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); | 270 | const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control); |
| 255 | if (control != nullptr) | 271 | if (control != nullptr) |
| 256 | GetMetadataFromControlNCA(patch, *control, icon, name); | 272 | GetMetadataFromControlNCA(patch, *control, icon, name); |
| 257 | 273 | ||
| 258 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, | 274 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, |
| 259 | compatibility_list, patch)); | 275 | compatibility_list, patch), |
| 276 | parent_dir); | ||
| 260 | } | 277 | } |
| 261 | } | 278 | } |
| 262 | 279 | ||
| 263 | void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, | 280 | void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, |
| 264 | unsigned int recursion) { | 281 | unsigned int recursion, GameListDir* parent_dir) { |
| 265 | const auto callback = [this, target, recursion](u64* num_entries_out, | 282 | const auto callback = [this, target, recursion, |
| 266 | const std::string& directory, | 283 | parent_dir](u64* num_entries_out, const std::string& directory, |
| 267 | const std::string& virtual_name) -> bool { | 284 | const std::string& virtual_name) -> bool { |
| 268 | if (stop_processing) { | 285 | if (stop_processing) { |
| 269 | // Breaks the callback loop. | 286 | // Breaks the callback loop. |
| 270 | return false; | 287 | return false; |
| @@ -317,11 +334,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 317 | const FileSys::PatchManager patch{program_id}; | 334 | const FileSys::PatchManager patch{program_id}; |
| 318 | 335 | ||
| 319 | emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, | 336 | emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, |
| 320 | compatibility_list, patch)); | 337 | compatibility_list, patch), |
| 338 | parent_dir); | ||
| 321 | } | 339 | } |
| 322 | } else if (is_dir && recursion > 0) { | 340 | } else if (is_dir && recursion > 0) { |
| 323 | watch_list.append(QString::fromStdString(physical_name)); | 341 | watch_list.append(QString::fromStdString(physical_name)); |
| 324 | ScanFileSystem(target, physical_name, recursion - 1); | 342 | ScanFileSystem(target, physical_name, recursion - 1, parent_dir); |
| 325 | } | 343 | } |
| 326 | 344 | ||
| 327 | return true; | 345 | return true; |
| @@ -332,12 +350,32 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 332 | 350 | ||
| 333 | void GameListWorker::run() { | 351 | void GameListWorker::run() { |
| 334 | stop_processing = false; | 352 | stop_processing = false; |
| 335 | watch_list.append(dir_path); | 353 | |
| 336 | provider->ClearAllEntries(); | 354 | for (UISettings::GameDir& game_dir : game_dirs) { |
| 337 | ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), | 355 | if (game_dir.path == QStringLiteral("SDMC")) { |
| 338 | deep_scan ? 256 : 0); | 356 | auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); |
| 339 | AddTitlesToGameList(); | 357 | emit DirEntryReady({game_list_dir}); |
| 340 | ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); | 358 | AddTitlesToGameList(game_list_dir); |
| 359 | } else if (game_dir.path == QStringLiteral("UserNAND")) { | ||
| 360 | auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); | ||
| 361 | emit DirEntryReady({game_list_dir}); | ||
| 362 | AddTitlesToGameList(game_list_dir); | ||
| 363 | } else if (game_dir.path == QStringLiteral("SysNAND")) { | ||
| 364 | auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); | ||
| 365 | emit DirEntryReady({game_list_dir}); | ||
| 366 | AddTitlesToGameList(game_list_dir); | ||
| 367 | } else { | ||
| 368 | watch_list.append(game_dir.path); | ||
| 369 | auto* const game_list_dir = new GameListDir(game_dir); | ||
| 370 | emit DirEntryReady({game_list_dir}); | ||
| 371 | provider->ClearAllEntries(); | ||
| 372 | ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2, | ||
| 373 | game_list_dir); | ||
| 374 | ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(), | ||
| 375 | game_dir.deep_scan ? 256 : 0, game_list_dir); | ||
| 376 | } | ||
| 377 | }; | ||
| 378 | |||
| 341 | emit Finished(watch_list); | 379 | emit Finished(watch_list); |
| 342 | } | 380 | } |
| 343 | 381 | ||
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 7c3074af9..6e52fca89 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <QObject> | 14 | #include <QObject> |
| 15 | #include <QRunnable> | 15 | #include <QRunnable> |
| 16 | #include <QString> | 16 | #include <QString> |
| 17 | #include <QVector> | ||
| 17 | 18 | ||
| 18 | #include "common/common_types.h" | 19 | #include "common/common_types.h" |
| 19 | #include "yuzu/compatibility_list.h" | 20 | #include "yuzu/compatibility_list.h" |
| @@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable { | |||
| 33 | Q_OBJECT | 34 | Q_OBJECT |
| 34 | 35 | ||
| 35 | public: | 36 | public: |
| 36 | GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, | 37 | explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, |
| 37 | FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, | 38 | FileSys::ManualContentProvider* provider, |
| 38 | const CompatibilityList& compatibility_list); | 39 | QVector<UISettings::GameDir>& game_dirs, |
| 40 | const CompatibilityList& compatibility_list); | ||
| 39 | ~GameListWorker() override; | 41 | ~GameListWorker() override; |
| 40 | 42 | ||
| 41 | /// Starts the processing of directory tree information. | 43 | /// Starts the processing of directory tree information. |
| @@ -48,31 +50,33 @@ signals: | |||
| 48 | /** | 50 | /** |
| 49 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready | 51 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready |
| 50 | * to be added to the game list. | 52 | * 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. | 53 | * @param entry_items a list with `QStandardItem`s that make up the columns of the new |
| 54 | * entry. | ||
| 52 | */ | 55 | */ |
| 53 | void EntryReady(QList<QStandardItem*> entry_items); | 56 | void DirEntryReady(GameListDir* entry_items); |
| 57 | void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir); | ||
| 54 | 58 | ||
| 55 | /** | 59 | /** |
| 56 | * After the worker has traversed the game directory looking for entries, this signal is emitted | 60 | * 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. | 61 | * emitted with a list of folders that should be watched for changes as well. |
| 58 | */ | 62 | */ |
| 59 | void Finished(QStringList watch_list); | 63 | void Finished(QStringList watch_list); |
| 60 | 64 | ||
| 61 | private: | 65 | private: |
| 62 | void AddTitlesToGameList(); | 66 | void AddTitlesToGameList(GameListDir* parent_dir); |
| 63 | 67 | ||
| 64 | enum class ScanTarget { | 68 | enum class ScanTarget { |
| 65 | FillManualContentProvider, | 69 | FillManualContentProvider, |
| 66 | PopulateGameList, | 70 | PopulateGameList, |
| 67 | }; | 71 | }; |
| 68 | 72 | ||
| 69 | void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); | 73 | void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion, |
| 74 | GameListDir* parent_dir); | ||
| 70 | 75 | ||
| 71 | std::shared_ptr<FileSys::VfsFilesystem> vfs; | 76 | std::shared_ptr<FileSys::VfsFilesystem> vfs; |
| 72 | FileSys::ManualContentProvider* provider; | 77 | FileSys::ManualContentProvider* provider; |
| 73 | QStringList watch_list; | 78 | QStringList watch_list; |
| 74 | QString dir_path; | ||
| 75 | bool deep_scan; | ||
| 76 | const CompatibilityList& compatibility_list; | 79 | const CompatibilityList& compatibility_list; |
| 80 | QVector<UISettings::GameDir>& game_dirs; | ||
| 77 | std::atomic_bool stop_processing; | 81 | std::atomic_bool stop_processing; |
| 78 | }; | 82 | }; |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ac57229d5..6d249cb3e 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,47 @@ 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(const QString& directory) { | ||
| 1313 | QString path; | ||
| 1314 | if (directory == QStringLiteral("SDMC")) { | ||
| 1315 | path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + | ||
| 1316 | "Nintendo/Contents/registered"); | ||
| 1317 | } else if (directory == QStringLiteral("UserNAND")) { | ||
| 1318 | path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + | ||
| 1319 | "user/Contents/registered"); | ||
| 1320 | } else if (directory == QStringLiteral("SysNAND")) { | ||
| 1321 | path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + | ||
| 1322 | "system/Contents/registered"); | ||
| 1323 | } else { | ||
| 1324 | path = directory; | ||
| 1325 | } | ||
| 1326 | if (!QFileInfo::exists(path)) { | ||
| 1327 | QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); | ||
| 1328 | return; | ||
| 1329 | } | ||
| 1330 | QDesktopServices::openUrl(QUrl::fromLocalFile(path)); | ||
| 1331 | } | ||
| 1332 | |||
| 1333 | void GMainWindow::OnGameListAddDirectory() { | ||
| 1334 | const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||
| 1335 | if (dir_path.isEmpty()) | ||
| 1336 | return; | ||
| 1337 | UISettings::GameDir game_dir{dir_path, false, true}; | ||
| 1338 | if (!UISettings::values.game_dirs.contains(game_dir)) { | ||
| 1339 | UISettings::values.game_dirs.append(game_dir); | ||
| 1340 | game_list->PopulateAsync(UISettings::values.game_dirs); | ||
| 1341 | } else { | ||
| 1342 | LOG_WARNING(Frontend, "Selected directory is already in the game list"); | ||
| 1343 | } | ||
| 1344 | } | ||
| 1345 | |||
| 1346 | void GMainWindow::OnGameListShowList(bool show) { | ||
| 1347 | if (emulation_running && ui.action_Single_Window_Mode->isChecked()) | ||
| 1348 | return; | ||
| 1349 | game_list->setVisible(show); | ||
| 1350 | game_list_placeholder->setVisible(!show); | ||
| 1351 | }; | ||
| 1352 | |||
| 1301 | void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { | 1353 | void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { |
| 1302 | u64 title_id{}; | 1354 | u64 title_id{}; |
| 1303 | const auto v_file = Core::GetGameFileFromPath(vfs, file); | 1355 | const auto v_file = Core::GetGameFileFromPath(vfs, file); |
| @@ -1316,8 +1368,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { | |||
| 1316 | 1368 | ||
| 1317 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); | 1369 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); |
| 1318 | if (reload) { | 1370 | if (reload) { |
| 1319 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1371 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1320 | UISettings::values.game_directory_deepscan); | ||
| 1321 | } | 1372 | } |
| 1322 | 1373 | ||
| 1323 | config->Save(); | 1374 | config->Save(); |
| @@ -1407,8 +1458,7 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 1407 | const auto success = [this]() { | 1458 | const auto success = [this]() { |
| 1408 | QMessageBox::information(this, tr("Successfully Installed"), | 1459 | QMessageBox::information(this, tr("Successfully Installed"), |
| 1409 | tr("The file was successfully installed.")); | 1460 | tr("The file was successfully installed.")); |
| 1410 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1461 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1411 | UISettings::values.game_directory_deepscan); | ||
| 1412 | FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + | 1462 | FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + |
| 1413 | DIR_SEP + "game_list"); | 1463 | DIR_SEP + "game_list"); |
| 1414 | }; | 1464 | }; |
| @@ -1533,14 +1583,6 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 1533 | } | 1583 | } |
| 1534 | } | 1584 | } |
| 1535 | 1585 | ||
| 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) { | 1586 | void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { |
| 1545 | const auto res = QMessageBox::information( | 1587 | const auto res = QMessageBox::information( |
| 1546 | this, tr("Changing Emulated Directory"), | 1588 | this, tr("Changing Emulated Directory"), |
| @@ -1559,8 +1601,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) | |||
| 1559 | : FileUtil::UserPath::NANDDir, | 1601 | : FileUtil::UserPath::NANDDir, |
| 1560 | dir_path.toStdString()); | 1602 | dir_path.toStdString()); |
| 1561 | Service::FileSystem::CreateFactories(*vfs); | 1603 | Service::FileSystem::CreateFactories(*vfs); |
| 1562 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1604 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1563 | UISettings::values.game_directory_deepscan); | ||
| 1564 | } | 1605 | } |
| 1565 | } | 1606 | } |
| 1566 | 1607 | ||
| @@ -1724,11 +1765,11 @@ void GMainWindow::OnConfigure() { | |||
| 1724 | if (UISettings::values.enable_discord_presence != old_discord_presence) { | 1765 | if (UISettings::values.enable_discord_presence != old_discord_presence) { |
| 1725 | SetDiscordEnabled(UISettings::values.enable_discord_presence); | 1766 | SetDiscordEnabled(UISettings::values.enable_discord_presence); |
| 1726 | } | 1767 | } |
| 1768 | emit UpdateThemedIcons(); | ||
| 1727 | 1769 | ||
| 1728 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); | 1770 | const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); |
| 1729 | if (reload) { | 1771 | if (reload) { |
| 1730 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 1772 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1731 | UISettings::values.game_directory_deepscan); | ||
| 1732 | } | 1773 | } |
| 1733 | 1774 | ||
| 1734 | config->Save(); | 1775 | config->Save(); |
| @@ -1992,8 +2033,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { | |||
| 1992 | Service::FileSystem::CreateFactories(*vfs); | 2033 | Service::FileSystem::CreateFactories(*vfs); |
| 1993 | 2034 | ||
| 1994 | if (behavior == ReinitializeKeyBehavior::Warning) { | 2035 | if (behavior == ReinitializeKeyBehavior::Warning) { |
| 1995 | game_list->PopulateAsync(UISettings::values.game_directory_path, | 2036 | game_list->PopulateAsync(UISettings::values.game_dirs); |
| 1996 | UISettings::values.game_directory_deepscan); | ||
| 1997 | } | 2037 | } |
| 1998 | } | 2038 | } |
| 1999 | 2039 | ||
| @@ -2158,7 +2198,6 @@ void GMainWindow::UpdateUITheme() { | |||
| 2158 | } | 2198 | } |
| 2159 | 2199 | ||
| 2160 | QIcon::setThemeSearchPaths(theme_paths); | 2200 | QIcon::setThemeSearchPaths(theme_paths); |
| 2161 | emit UpdateThemedIcons(); | ||
| 2162 | } | 2201 | } |
| 2163 | 2202 | ||
| 2164 | void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { | 2203 | void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 501608ddc..7d16188cb 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(const QString& directory); | ||
| 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..c57290006 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h | |||
| @@ -8,8 +8,10 @@ | |||
| 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> |
| 14 | #include <QVector> | ||
| 13 | #include "common/common_types.h" | 15 | #include "common/common_types.h" |
| 14 | 16 | ||
| 15 | namespace UISettings { | 17 | namespace UISettings { |
| @@ -25,6 +27,18 @@ struct Shortcut { | |||
| 25 | using Themes = std::array<std::pair<const char*, const char*>, 2>; | 27 | using Themes = std::array<std::pair<const char*, const char*>, 2>; |
| 26 | extern const Themes themes; | 28 | extern const Themes themes; |
| 27 | 29 | ||
| 30 | struct GameDir { | ||
| 31 | QString path; | ||
| 32 | bool deep_scan; | ||
| 33 | bool expanded; | ||
| 34 | bool operator==(const GameDir& rhs) const { | ||
| 35 | return path == rhs.path; | ||
| 36 | }; | ||
| 37 | bool operator!=(const GameDir& rhs) const { | ||
| 38 | return !operator==(rhs); | ||
| 39 | }; | ||
| 40 | }; | ||
| 41 | |||
| 28 | struct Values { | 42 | struct Values { |
| 29 | QByteArray geometry; | 43 | QByteArray geometry; |
| 30 | QByteArray state; | 44 | QByteArray state; |
| @@ -55,8 +69,9 @@ struct Values { | |||
| 55 | QString roms_path; | 69 | QString roms_path; |
| 56 | QString symbols_path; | 70 | QString symbols_path; |
| 57 | QString screenshot_path; | 71 | QString screenshot_path; |
| 58 | QString game_directory_path; | 72 | QString game_dir_deprecated; |
| 59 | bool game_directory_deepscan; | 73 | bool game_dir_deprecated_deepscan; |
| 74 | QVector<UISettings::GameDir> game_dirs; | ||
| 60 | QStringList recent_files; | 75 | QStringList recent_files; |
| 61 | 76 | ||
| 62 | QString theme; | 77 | QString theme; |
| @@ -84,3 +99,5 @@ struct Values { | |||
| 84 | 99 | ||
| 85 | extern Values values; | 100 | extern Values values; |
| 86 | } // namespace UISettings | 101 | } // namespace UISettings |
| 102 | |||
| 103 | Q_DECLARE_METATYPE(UISettings::GameDir*); | ||