diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 92e56119c0..8192c5f3e4 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -3120,6 +3120,8 @@ void Emulator::AddGamesFromDir(const std::string& path) // Nothing to do } + process_qt_events(); + // search direct subdirectories, that way we can drop one folder containing all games for (auto&& dir_entry : fs::dir(path)) { @@ -3134,6 +3136,8 @@ void Emulator::AddGamesFromDir(const std::string& path) { // Nothing to do } + + process_qt_events(); } m_games_config.set_save_on_dirty(true); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 09c7ab1336..b5e04b4ce9 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -73,6 +73,18 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std m_gui_settings->SetValue(gui::gl_marginFactor, m_margin_factor); m_gui_settings->SetValue(gui::gl_textFactor, m_text_factor); + // Only show the progress dialog after some time has passed + m_progress_dialog_timer = new QTimer(this); + m_progress_dialog_timer->setSingleShot(true); + m_progress_dialog_timer->setInterval(200); + connect(m_progress_dialog_timer, &QTimer::timeout, this, [this]() + { + if (m_progress_dialog) + { + m_progress_dialog->show(); + } + }); + m_game_dock = new QMainWindow(this); m_game_dock->setWindowFlags(Qt::Widget); setWidget(m_game_dock); @@ -137,6 +149,18 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std add_column(gui::column_dir_size, tr("Space On Disk"), tr("Show Space On Disk")); // Events + connect(&m_parsing_watcher, &QFutureWatcher::finished, this, &game_list_frame::OnParsingFinished); + connect(&m_parsing_watcher, &QFutureWatcher::canceled, this, [this]() + { + WaitAndAbortSizeCalcThreads(); + WaitAndAbortRepaintThreads(); + + m_path_entries.clear(); + m_path_list.clear(); + m_game_data.clear(); + m_serials.clear(); + m_games.pop_all(); + }); connect(&m_refresh_watcher, &QFutureWatcher::finished, this, &game_list_frame::OnRefreshFinished); connect(&m_refresh_watcher, &QFutureWatcher::canceled, this, [this]() { @@ -241,32 +265,19 @@ void game_list_frame::LoadSettings() m_show_custom_icons = m_gui_settings->GetValue(gui::gl_custom_icon).toBool(); m_play_hover_movies = m_gui_settings->GetValue(gui::gl_hover_gifs).toBool(); - Refresh(true); - - const QByteArray state = m_gui_settings->GetValue(gui::gl_state).toByteArray(); - if (!m_game_list->horizontalHeader()->restoreState(state) && m_game_list->rowCount()) - { - // If no settings exist, resize to contents. - ResizeColumnsToContents(); - } - for (int col = 0; col < m_columnActs.count(); ++col) { const bool vis = m_gui_settings->GetGamelistColVisibility(col); m_columnActs[col]->setChecked(vis); m_game_list->setColumnHidden(col, !vis); } - - SortGameList(); - FixNarrowColumns(); - - m_game_list->horizontalHeader()->restoreState(m_game_list->horizontalHeader()->saveState()); } game_list_frame::~game_list_frame() { WaitAndAbortSizeCalcThreads(); WaitAndAbortRepaintThreads(); + gui::utils::stop_future_watcher(m_parsing_watcher, true); gui::utils::stop_future_watcher(m_refresh_watcher, true); SaveSettings(); @@ -316,7 +327,7 @@ void game_list_frame::ResizeColumnsToContents(int spacing) const void game_list_frame::OnColClicked(int col) { - if (col == 0) return; // Don't "sort" icons. + if (col == gui::column_icon) return; // Don't "sort" icons. if (col == m_sort_column) { @@ -352,27 +363,31 @@ bool game_list_frame::IsEntryVisible(const game_info& game, bool search_fallback return is_visible && matches_category() && SearchMatchesApp(qstr(game->info.name), serial, search_fallback); } -void game_list_frame::SortGameList() const +void game_list_frame::SortGameList() { + gui::utils::stop_future_watcher(m_parsing_watcher, false); + gui::utils::stop_future_watcher(m_refresh_watcher, false); + WaitAndAbortRepaintThreads(); + // Back-up old header sizes to handle unwanted column resize in case of zero search results - QList column_widths; const int old_row_count = m_game_list->rowCount(); const int old_game_count = m_game_data.count(); + std::vector column_widths(m_game_list->columnCount()); for (int i = 0; i < m_game_list->columnCount(); i++) { - column_widths.append(m_game_list->columnWidth(i)); + column_widths[i] = m_game_list->columnWidth(i); } // Sorting resizes hidden columns, so unhide them as a workaround - QList columns_to_hide; + std::vector columns_to_hide; for (int i = 0; i < m_game_list->columnCount(); i++) { if (m_game_list->isColumnHidden(i)) { m_game_list->setColumnHidden(i, false); - columns_to_hide << i; + columns_to_hide.push_back(i); } } @@ -380,9 +395,9 @@ void game_list_frame::SortGameList() const m_game_list->sortByColumn(m_sort_column, m_col_sort_order); // Hide columns again - for (auto i : columns_to_hide) + for (int col : columns_to_hide) { - m_game_list->setColumnHidden(i, true); + m_game_list->setColumnHidden(col, true); } // Don't resize the columns if no game is shown to preserve the header settings @@ -434,6 +449,18 @@ std::string game_list_frame::GetDataDirBySerial(const std::string& serial) return fs::get_config_dir() + "data/" + serial; } +void game_list_frame::push_path(const std::string& path, std::vector& legit_paths) +{ + { + std::lock_guard lock(m_path_mutex); + if (!m_path_list.insert(path).second) + { + return; + } + } + legit_paths.push_back(path); +} + void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) { if (from_drive) @@ -441,12 +468,23 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) WaitAndAbortSizeCalcThreads(); } WaitAndAbortRepaintThreads(); + gui::utils::stop_future_watcher(m_parsing_watcher, from_drive); gui::utils::stop_future_watcher(m_refresh_watcher, from_drive); + if (m_progress_dialog_timer) + { + m_progress_dialog_timer->stop(); + } + + if (m_progress_dialog) + { + m_progress_dialog->accept(); + m_progress_dialog->deleteLater(); + m_progress_dialog = nullptr; + } + if (from_drive) { - const Localized localized; - m_path_entries.clear(); m_path_list.clear(); m_serials.clear(); @@ -454,299 +492,87 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) m_notes.clear(); m_games.pop_all(); + m_progress_dialog = new progress_dialog(tr("Loading games"), tr("Loading games, please wait..."), tr("Cancel"), 0, 0, true, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + connect(&m_refresh_watcher, &QFutureWatcher::progressRangeChanged, m_progress_dialog, &QProgressDialog::setRange); + connect(&m_refresh_watcher, &QFutureWatcher::progressValueChanged, m_progress_dialog, &QProgressDialog::setValue); + connect(m_progress_dialog, &QProgressDialog::canceled, this, [this]() + { + gui::utils::stop_future_watcher(m_parsing_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, true); + + m_path_entries.clear(); + m_path_list.clear(); + m_serials.clear(); + m_game_data.clear(); + m_notes.clear(); + m_games.pop_all(); + + if (m_progress_dialog_timer) + { + m_progress_dialog_timer->stop(); + } + + m_progress_dialog->deleteLater(); + m_progress_dialog = nullptr; + }); + + if (m_progress_dialog_timer) + { + m_progress_dialog_timer->start(); + } + if (Emu.IsStopped()) { Emu.AddGamesFromDir(g_cfg_vfs.get(g_cfg_vfs.games_dir, rpcs3::utils::get_emu_dir())); } - const std::string _hdd = rpcs3::utils::get_hdd0_dir(); + const std::string _hdd = rpcs3::utils::get_hdd0_dir(); - const auto push_path = [this](const std::string& path, std::vector& legit_paths) + m_parsing_watcher.setFuture(QtConcurrent::map(m_parsing_threads, [this, _hdd](int index) { + if (index > 0) { + game_list_log.error("Unexpected thread index: %d", index); + return; + } + + const auto add_dir = [this](const std::string& path, bool is_disc) + { + for (const auto& entry : fs::dir(path)) + { + if (!entry.is_directory || entry.name == "." || entry.name == "..") + { + continue; + } + + QApplication::processEvents(); + std::lock_guard lock(m_path_mutex); + m_path_entries.emplace_back(path_entry{path + entry.name, is_disc, false}); + } + }; + + add_dir(_hdd + "game/", false); + add_dir(_hdd + "disc/", true); // Deprecated + + for (const auto& [serial, path] : Emu.GetGamesConfig().get_games()) + { + std::string game_dir = path; + game_dir.resize(game_dir.find_last_not_of('/') + 1); + + if (game_dir.empty()) + { + continue; + } + + // Don't use the C00 subdirectory in our game list + if (game_dir.ends_with("/C00") || game_dir.ends_with("\\C00")) + { + game_dir = game_dir.substr(0, game_dir.size() - 4); + } + + QApplication::processEvents(); std::lock_guard lock(m_path_mutex); - if (!m_path_list.insert(path).second) - { - return; - } - } - legit_paths.push_back(path); - }; - - const auto add_disc_dir = [push_path](const std::string& path, std::vector& legit_paths) - { - for (const auto& entry : fs::dir(path)) - { - if (!entry.is_directory || entry.name == "." || entry.name == "..") - { - continue; - } - - if (entry.name == "PS3_GAME" || std::regex_match(entry.name, std::regex("^PS3_GM[[:digit:]]{2}$"))) - { - push_path(path + "/" + entry.name, legit_paths); - } - } - }; - - const auto add_dir = [&](const std::string& path, bool is_disc) - { - for (const auto& entry : fs::dir(path)) - { - if (!entry.is_directory || entry.name == "." || entry.name == "..") - { - continue; - } - - m_path_entries.emplace_back(path_entry{path + entry.name, is_disc, false}); - } - }; - - add_dir(_hdd + "game/", false); - add_dir(_hdd + "disc/", true); // Deprecated - - for (const auto& [serial, path] : Emu.GetGamesConfig().get_games()) - { - std::string game_dir = path; - - game_dir.resize(game_dir.find_last_not_of('/') + 1); - - if (game_dir.empty()) - { - continue; - } - - // Don't use the C00 subdirectory in our game list - if (game_dir.ends_with("/C00") || game_dir.ends_with("\\C00")) - { - game_dir = game_dir.substr(0, game_dir.size() - 4); - } - - m_path_entries.emplace_back(path_entry{game_dir, false, true}); - } - - const auto dev_flash = g_cfg_vfs.get_dev_flash(); - - m_path_entries.emplace_back(path_entry{dev_flash + "vsh/module/vsh.self", false, false}); - - // Remove duplicates - sort(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path < r.path;}); - m_path_entries.erase(unique(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path == r.path;}), m_path_entries.end()); - - const std::string game_icon_path = m_play_hover_movies ? fs::get_config_dir() + "/Icons/game_icons/" : ""; - - const auto add_game = [this, dev_flash, cat_unknown_localized = sstr(localized.category.unknown), cat_unknown = sstr(cat::cat_unknown), game_icon_path](const std::string& dir_or_elf) - { - GameInfo game{}; - game.path = dir_or_elf; - - const Localized thread_localized; - - const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); - const psf::registry psf = psf::load_object(sfo_dir + "/PARAM.SFO"); - const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); - - if (title_id.empty()) - { - if (!fs::is_file(dir_or_elf)) - { - // Do not care about invalid entries - return; - } - - game.serial = dir_or_elf.substr(dir_or_elf.find_last_of(fs::delim) + 1); - game.category = cat::cat_ps3_os.toStdString(); // Key for operating system executables - game.version = utils::get_firmware_version(); - game.fw = game.version; - game.bootable = 1; - game.icon_path = dev_flash + "vsh/resource/explore/icon/icon_home.png"; - - if (dir_or_elf.starts_with(dev_flash)) - { - std::string path_vfs = dir_or_elf.substr(dev_flash.size()); - - if (const usz pos = path_vfs.find_first_not_of(fs::delim); pos != umax && pos != 0) - { - path_vfs = path_vfs.substr(pos); - } - - if (const auto it = thread_localized.title.titles.find(path_vfs); it != thread_localized.title.titles.cend()) - { - game.name = it->second.toStdString(); - } - } - - if (game.name == "Unknown") - { - game.name = game.serial; - } - } - else - { - game.serial = std::string(title_id); - game.name = std::string(psf::get_string(psf, "TITLE", cat_unknown_localized)); - game.app_ver = std::string(psf::get_string(psf, "APP_VER", cat_unknown_localized)); - game.version = std::string(psf::get_string(psf, "VERSION", cat_unknown_localized)); - game.category = std::string(psf::get_string(psf, "CATEGORY", cat_unknown)); - game.fw = std::string(psf::get_string(psf, "PS3_SYSTEM_VER", cat_unknown_localized)); - game.parental_lvl = psf::get_integer(psf, "PARENTAL_LEVEL", 0); - game.resolution = psf::get_integer(psf, "RESOLUTION", 0); - game.sound_format = psf::get_integer(psf, "SOUND_FORMAT", 0); - game.bootable = psf::get_integer(psf, "BOOTABLE", 0); - game.attr = psf::get_integer(psf, "ATTRIBUTE", 0); - game.icon_path = sfo_dir + "/ICON0.PNG"; - - if (game.category == "DG") - { - std::string latest_icon = rpcs3::utils::get_hdd0_dir() + "game/" + game.serial + "/ICON0.PNG"; - if (fs::is_file(latest_icon)) - { - game.icon_path = std::move(latest_icon); - } - } - } - - if (m_show_custom_icons) - { - if (std::string icon_path = fs::get_config_dir() + "/Icons/game_icons/" + game.serial + "/ICON0.PNG"; fs::is_file(icon_path)) - { - game.icon_path = std::move(icon_path); - } - } - - const QString serial = qstr(game.serial); - - m_games_mutex.lock(); - - // Read persistent_settings values - const QString note = m_persistent_settings->GetValue(gui::persistent::notes, serial, "").toString(); - const QString title = m_persistent_settings->GetValue(gui::persistent::titles, serial, "").toString().simplified(); - QString last_played = m_persistent_settings->GetValue(gui::persistent::last_played, serial, "").toString(); - quint64 playtime = m_persistent_settings->GetValue(gui::persistent::playtime, serial, 0).toULongLong(); - - // Set persistent_settings values if values exist - if (!last_played.isEmpty()) - { - m_persistent_settings->SetLastPlayed(serial, last_played); - } - if (playtime > 0) - { - m_persistent_settings->SetPlaytime(serial, playtime); - } - - m_serials.insert(serial); - - if (!note.isEmpty()) - { - m_notes.insert(serial, note); - } - - if (!title.isEmpty()) - { - m_titles.insert(serial, title); - } - - m_games_mutex.unlock(); - - QString qt_cat = qstr(game.category); - - if (const auto boot_cat = thread_localized.category.cat_boot.find(qt_cat); boot_cat != thread_localized.category.cat_boot.cend()) - { - qt_cat = boot_cat->second; - } - else if (const auto data_cat = thread_localized.category.cat_data.find(qt_cat); data_cat != thread_localized.category.cat_data.cend()) - { - qt_cat = data_cat->second; - } - else if (game.category == cat_unknown) - { - qt_cat = thread_localized.category.unknown; - } - else - { - qt_cat = thread_localized.category.other; - } - - gui_game_info info{}; - info.info = game; - info.localized_category = std::move(qt_cat); - info.compat = m_game_compat->GetCompatibility(game.serial); - info.hasCustomConfig = fs::is_file(rpcs3::utils::get_custom_config_path(game.serial)); - info.hasCustomPadConfig = fs::is_file(rpcs3::utils::get_custom_input_config_path(game.serial)); - info.has_hover_gif = fs::is_file(game_icon_path + game.serial + "/hover.gif"); - - m_games.push(std::make_shared(std::move(info))); - }; - - m_refresh_watcher.setFuture(QtConcurrent::map(m_path_entries, [this, _hdd, add_disc_dir, push_path, add_game](const path_entry& entry) - { - std::vector legit_paths; - - if (entry.is_from_yml) - { - if (fs::is_file(entry.path + "/PARAM.SFO")) - { - push_path(entry.path, legit_paths); - } - else if (fs::is_file(entry.path + "/PS3_DISC.SFB")) - { - // Check if a path loaded from games.yml is already registered in add_dir(_hdd + "disc/"); - if (entry.path.starts_with(_hdd)) - { - std::string_view frag = std::string_view(entry.path).substr(_hdd.size()); - - if (frag.starts_with("disc/")) - { - // Our path starts from _hdd + 'disc/' - frag.remove_prefix(5); - - // Check if the remaining part is the only path component - if (frag.find_first_of('/') + 1 == 0) - { - game_list_log.trace("Removed duplicate: %s", entry.path); - - if (static std::unordered_set warn_once_list; warn_once_list.emplace(entry.path).second) - { - game_list_log.todo("Game at '%s' is using deprecated directory '/dev_hdd0/disc/'.\nConsider moving into '%s'.", entry.path, g_cfg_vfs.get(g_cfg_vfs.games_dir, rpcs3::utils::get_emu_dir())); - } - - return; - } - } - } - - add_disc_dir(entry.path, legit_paths); - } - else - { - game_list_log.trace("Invalid game path registered: %s", entry.path); - return; - } - } - else if (fs::is_file(entry.path + "/PS3_DISC.SFB")) - { - if (!entry.is_disc) - { - game_list_log.error("Invalid game path found in %s", entry.path); - return; - } - - add_disc_dir(entry.path, legit_paths); - } - else - { - if (entry.is_disc) - { - game_list_log.error("Invalid disc path found in %s", entry.path); - return; - } - - push_path(entry.path, legit_paths); - } - - for (const std::string& path : legit_paths) - { - add_game(path); + m_path_entries.emplace_back(path_entry{game_dir, false, true}); } })); @@ -777,6 +603,252 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) } } +void game_list_frame::OnParsingFinished() +{ + const Localized localized; + const std::string dev_flash = g_cfg_vfs.get_dev_flash(); + const std::string _hdd = rpcs3::utils::get_hdd0_dir(); + + m_path_entries.emplace_back(path_entry{dev_flash + "vsh/module/vsh.self", false, false}); + + // Remove duplicates + sort(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path < r.path;}); + m_path_entries.erase(unique(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path == r.path;}), m_path_entries.end()); + + const std::string game_icon_path = m_play_hover_movies ? fs::get_config_dir() + "/Icons/game_icons/" : ""; + + const auto add_game = [this, dev_flash, cat_unknown_localized = sstr(localized.category.unknown), cat_unknown = sstr(cat::cat_unknown), game_icon_path](const std::string& dir_or_elf) + { + GameInfo game{}; + game.path = dir_or_elf; + + const Localized thread_localized; + + const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); + const psf::registry psf = psf::load_object(sfo_dir + "/PARAM.SFO"); + const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); + + if (title_id.empty()) + { + if (!fs::is_file(dir_or_elf)) + { + // Do not care about invalid entries + return; + } + + game.serial = dir_or_elf.substr(dir_or_elf.find_last_of(fs::delim) + 1); + game.category = cat::cat_ps3_os.toStdString(); // Key for operating system executables + game.version = utils::get_firmware_version(); + game.fw = game.version; + game.bootable = 1; + game.icon_path = dev_flash + "vsh/resource/explore/icon/icon_home.png"; + + if (dir_or_elf.starts_with(dev_flash)) + { + std::string path_vfs = dir_or_elf.substr(dev_flash.size()); + + if (const usz pos = path_vfs.find_first_not_of(fs::delim); pos != umax && pos != 0) + { + path_vfs = path_vfs.substr(pos); + } + + if (const auto it = thread_localized.title.titles.find(path_vfs); it != thread_localized.title.titles.cend()) + { + game.name = it->second.toStdString(); + } + } + + if (game.name == "Unknown") + { + game.name = game.serial; + } + } + else + { + game.serial = std::string(title_id); + game.name = std::string(psf::get_string(psf, "TITLE", cat_unknown_localized)); + game.app_ver = std::string(psf::get_string(psf, "APP_VER", cat_unknown_localized)); + game.version = std::string(psf::get_string(psf, "VERSION", cat_unknown_localized)); + game.category = std::string(psf::get_string(psf, "CATEGORY", cat_unknown)); + game.fw = std::string(psf::get_string(psf, "PS3_SYSTEM_VER", cat_unknown_localized)); + game.parental_lvl = psf::get_integer(psf, "PARENTAL_LEVEL", 0); + game.resolution = psf::get_integer(psf, "RESOLUTION", 0); + game.sound_format = psf::get_integer(psf, "SOUND_FORMAT", 0); + game.bootable = psf::get_integer(psf, "BOOTABLE", 0); + game.attr = psf::get_integer(psf, "ATTRIBUTE", 0); + game.icon_path = sfo_dir + "/ICON0.PNG"; + + if (game.category == "DG") + { + std::string latest_icon = rpcs3::utils::get_hdd0_dir() + "game/" + game.serial + "/ICON0.PNG"; + if (fs::is_file(latest_icon)) + { + game.icon_path = std::move(latest_icon); + } + } + } + + if (m_show_custom_icons) + { + if (std::string icon_path = fs::get_config_dir() + "/Icons/game_icons/" + game.serial + "/ICON0.PNG"; fs::is_file(icon_path)) + { + game.icon_path = std::move(icon_path); + } + } + + const QString serial = qstr(game.serial); + + m_games_mutex.lock(); + + // Read persistent_settings values + const QString note = m_persistent_settings->GetValue(gui::persistent::notes, serial, "").toString(); + const QString title = m_persistent_settings->GetValue(gui::persistent::titles, serial, "").toString().simplified(); + QString last_played = m_persistent_settings->GetValue(gui::persistent::last_played, serial, "").toString(); + quint64 playtime = m_persistent_settings->GetValue(gui::persistent::playtime, serial, 0).toULongLong(); + + // Set persistent_settings values if values exist + if (!last_played.isEmpty()) + { + m_persistent_settings->SetLastPlayed(serial, last_played); + } + if (playtime > 0) + { + m_persistent_settings->SetPlaytime(serial, playtime); + } + + m_serials.insert(serial); + + if (!note.isEmpty()) + { + m_notes.insert(serial, note); + } + + if (!title.isEmpty()) + { + m_titles.insert(serial, title); + } + + m_games_mutex.unlock(); + + QString qt_cat = qstr(game.category); + + if (const auto boot_cat = thread_localized.category.cat_boot.find(qt_cat); boot_cat != thread_localized.category.cat_boot.cend()) + { + qt_cat = boot_cat->second; + } + else if (const auto data_cat = thread_localized.category.cat_data.find(qt_cat); data_cat != thread_localized.category.cat_data.cend()) + { + qt_cat = data_cat->second; + } + else if (game.category == cat_unknown) + { + qt_cat = thread_localized.category.unknown; + } + else + { + qt_cat = thread_localized.category.other; + } + + gui_game_info info{}; + info.info = std::move(game); + info.localized_category = std::move(qt_cat); + info.compat = m_game_compat->GetCompatibility(info.info.serial); + info.hasCustomConfig = fs::is_file(rpcs3::utils::get_custom_config_path(info.info.serial)); + info.hasCustomPadConfig = fs::is_file(rpcs3::utils::get_custom_input_config_path(info.info.serial)); + info.has_hover_gif = fs::is_file(game_icon_path + info.info.serial + "/hover.gif"); + + m_games.push(std::make_shared(std::move(info))); + }; + + const auto add_disc_dir = [this](const std::string& path, std::vector& legit_paths) + { + for (const auto& entry : fs::dir(path)) + { + if (!entry.is_directory || entry.name == "." || entry.name == "..") + { + continue; + } + + if (entry.name == "PS3_GAME" || std::regex_match(entry.name, std::regex("^PS3_GM[[:digit:]]{2}$"))) + { + push_path(path + "/" + entry.name, legit_paths); + } + } + }; + + m_refresh_watcher.setFuture(QtConcurrent::map(m_path_entries, [this, _hdd, add_disc_dir, add_game](const path_entry& entry) + { + std::vector legit_paths; + + if (entry.is_from_yml) + { + if (fs::is_file(entry.path + "/PARAM.SFO")) + { + push_path(entry.path, legit_paths); + } + else if (fs::is_file(entry.path + "/PS3_DISC.SFB")) + { + // Check if a path loaded from games.yml is already registered in add_dir(_hdd + "disc/"); + if (entry.path.starts_with(_hdd)) + { + std::string_view frag = std::string_view(entry.path).substr(_hdd.size()); + + if (frag.starts_with("disc/")) + { + // Our path starts from _hdd + 'disc/' + frag.remove_prefix(5); + + // Check if the remaining part is the only path component + if (frag.find_first_of('/') + 1 == 0) + { + game_list_log.trace("Removed duplicate: %s", entry.path); + + if (static std::unordered_set warn_once_list; warn_once_list.emplace(entry.path).second) + { + game_list_log.todo("Game at '%s' is using deprecated directory '/dev_hdd0/disc/'.\nConsider moving into '%s'.", entry.path, g_cfg_vfs.get(g_cfg_vfs.games_dir, rpcs3::utils::get_emu_dir())); + } + + return; + } + } + } + + add_disc_dir(entry.path, legit_paths); + } + else + { + game_list_log.trace("Invalid game path registered: %s", entry.path); + return; + } + } + else if (fs::is_file(entry.path + "/PS3_DISC.SFB")) + { + if (!entry.is_disc) + { + game_list_log.error("Invalid game path found in %s", entry.path); + return; + } + + add_disc_dir(entry.path, legit_paths); + } + else + { + if (entry.is_disc) + { + game_list_log.error("Invalid disc path found in %s", entry.path); + return; + } + + push_path(entry.path, legit_paths); + } + + for (const std::string& path : legit_paths) + { + add_game(path); + } + })); +} + void game_list_frame::OnRefreshFinished() { WaitAndAbortSizeCalcThreads(); @@ -851,6 +923,32 @@ void game_list_frame::OnRefreshFinished() m_path_entries.clear(); Refresh(); + + if (!std::exchange(m_initial_refresh_done, true)) + { + const QByteArray state = m_gui_settings->GetValue(gui::gl_state).toByteArray(); + if (!m_game_list->horizontalHeader()->restoreState(state) && m_game_list->rowCount()) + { + // If no settings exist, resize to contents. + ResizeColumnsToContents(); + } + + FixNarrowColumns(); + + m_game_list->horizontalHeader()->restoreState(m_game_list->horizontalHeader()->saveState()); + } + + if (m_progress_dialog_timer) + { + m_progress_dialog_timer->stop(); + } + + if (m_progress_dialog) + { + m_progress_dialog->accept(); + m_progress_dialog->deleteLater(); + m_progress_dialog = nullptr; + } } void game_list_frame::OnCompatFinished() @@ -2338,6 +2436,8 @@ void game_list_frame::ResizeIcons(const int& slider_pos) void game_list_frame::RepaintIcons(const bool& from_settings) { + gui::utils::stop_future_watcher(m_parsing_watcher, false); + gui::utils::stop_future_watcher(m_refresh_watcher, false); WaitAndAbortRepaintThreads(); if (from_settings) @@ -2744,7 +2844,7 @@ void game_list_frame::PopulateGameGrid(int maxCols, const QSize& image_size, con } // Get list of matching apps - QList matching_apps; + std::vector matching_apps; for (const auto& app : m_game_data) { @@ -2757,7 +2857,7 @@ void game_list_frame::PopulateGameGrid(int maxCols, const QSize& image_size, con } // Fallback is not needed when at least one entry is visible - if (matching_apps.isEmpty()) + if (matching_apps.empty()) { for (const auto& app : m_game_data) { @@ -2768,7 +2868,7 @@ void game_list_frame::PopulateGameGrid(int maxCols, const QSize& image_size, con } } - const int entries = matching_apps.count(); + const int entries = static_cast(matching_apps.size()); // Edge cases! if (entries == 0) diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index f3475c6aa5..b09c8f5ade 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -21,6 +22,7 @@ class game_list_grid; class gui_settings; class emu_settings; class persistent_settings; +class progress_dialog; class game_list_frame : public custom_dock_widget { @@ -80,6 +82,7 @@ public Q_SLOTS: void FocusAndSelectFirstEntryIfNoneIs(); private Q_SLOTS: + void OnParsingFinished(); void OnRefreshFinished(); void OnCompatFinished(); void OnColClicked(int col); @@ -101,6 +104,8 @@ protected: void resizeEvent(QResizeEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; private: + void push_path(const std::string& path, std::vector& legit_paths); + QPixmap PaintedPixmap(const QPixmap& icon, bool paint_config_icon = false, bool paint_pad_config_icon = false, const QColor& color = QColor()) const; QColor getGridCompatibilityColor(const QString& string) const; void IconLoadFunction(game_info game, std::shared_ptr> cancel); @@ -111,7 +116,7 @@ private: void PopulateGameList(); void PopulateGameGrid(int maxCols, const QSize& image_size, const QColor& image_color); bool IsEntryVisible(const game_info& game, bool search_fallback = false); - void SortGameList() const; + void SortGameList(); bool SearchMatchesApp(const QString& name, const QString& serial, bool fallback = false) const; bool RemoveCustomConfiguration(const std::string& title_id, const game_info& game = nullptr, bool is_interactive = false); @@ -144,9 +149,12 @@ private: // Game List game_list* m_game_list = nullptr; game_compatibility* m_game_compat = nullptr; + progress_dialog* m_progress_dialog = nullptr; + QTimer* m_progress_dialog_timer = nullptr; QList m_columnActs; Qt::SortOrder m_col_sort_order; int m_sort_column; + bool m_initial_refresh_done = false; QMap m_notes; QMap m_titles; @@ -175,6 +183,8 @@ private: QSet m_serials; QMutex m_games_mutex; lf_queue m_games; + const std::array m_parsing_threads{0}; + QFutureWatcher m_parsing_watcher; QFutureWatcher m_refresh_watcher; QSet m_hidden_list; bool m_show_hidden{false}; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 1b4e93a704..927d3e246f 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -192,9 +192,6 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) connect(m_thumb_playPause, &QWinThumbnailToolButton::clicked, this, &main_window::OnPlayOrPause); #endif - // Fix possible hidden game list columns. The game list has to be visible already. Use this after show() - m_game_list_frame->FixNarrowColumns(); - // RPCS3 Updater QMenu* download_menu = new QMenu(tr("Update Available!")); @@ -249,6 +246,9 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) // Focus to search bar by default ui->mw_searchbar->setFocus(); + // Refresh gamelist last + m_game_list_frame->Refresh(true); + return true; } diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index 9dc598204e..02f9ac6640 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -120,7 +120,6 @@ private Q_SLOTS: static void show_boot_error(game_boot_result status); void SaveWindowState() const; - void ConfigureGuiFromSettings(); void SetIconSizeActions(int idx) const; void ResizeIcons(int index); @@ -139,6 +138,7 @@ protected: void dragLeaveEvent(QDragLeaveEvent* event) override; private: + void ConfigureGuiFromSettings(); void RepaintToolBarIcons(); void RepaintThumbnailIcons(); void CreateActions(); diff --git a/rpcs3/rpcs3qt/save_manager_dialog.cpp b/rpcs3/rpcs3qt/save_manager_dialog.cpp index 5d755f6e3e..07e9a8caf4 100644 --- a/rpcs3/rpcs3qt/save_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/save_manager_dialog.cpp @@ -5,6 +5,7 @@ #include "gui_settings.h" #include "persistent_settings.h" #include "game_list_delegate.h" +#include "progress_dialog.h" #include "Emu/System.h" #include "Emu/system_utils.hpp" @@ -22,7 +23,6 @@ #include #include #include -#include #include "Utilities/File.h" #include "Utilities/mutex.h" @@ -237,8 +237,7 @@ std::vector save_manager_dialog::GetSaveEntries(const std::string QFutureWatcher future_watcher; - QProgressDialog progress_dialog(tr("Loading save data, please wait..."), tr("Cancel"), 0, 1, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); - progress_dialog.setWindowTitle(tr("Loading save data")); + progress_dialog progress_dialog(tr("Loading save data"), tr("Loading save data, please wait..."), tr("Cancel"), 0, 1, false, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); connect(&future_watcher, &QFutureWatcher::progressRangeChanged, &progress_dialog, &QProgressDialog::setRange); connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress_dialog, &QProgressDialog::setValue); diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index 6da9230f1a..d6b672e906 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -5,6 +5,7 @@ #include "qt_utils.h" #include "game_list.h" #include "gui_settings.h" +#include "progress_dialog.h" #include "util/logs.hpp" #include "Utilities/StrUtil.h" @@ -29,7 +30,6 @@ #include #include #include -#include #include #include @@ -919,8 +919,7 @@ void trophy_manager_dialog::StartTrophyLoadThreads() QFutureWatcher futureWatcher; - QProgressDialog progressDialog(tr("Loading trophy data, please wait..."), tr("Cancel"), 0, 1, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); - progressDialog.setWindowTitle(tr("Loading trophies")); + progress_dialog progressDialog(tr("Loading trophies"), tr("Loading trophy data, please wait..."), tr("Cancel"), 0, 1, false, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); connect(&futureWatcher, &QFutureWatcher::progressRangeChanged, &progressDialog, &QProgressDialog::setRange); connect(&futureWatcher, &QFutureWatcher::progressValueChanged, &progressDialog, &QProgressDialog::setValue);