diff --git a/dep/imgui/include/IconsEmoji.h b/dep/imgui/include/IconsEmoji.h index 56d2391cf..763f0402d 100644 --- a/dep/imgui/include/IconsEmoji.h +++ b/dep/imgui/include/IconsEmoji.h @@ -52,3 +52,8 @@ #define ICON_EMOJI_CHART_UPWARDS_TREND "\xf0\x9f\x93\x88" #define ICON_EMOJI_DIRECT_HIT "\xf0\x9f\x8e\xaf" #define ICON_EMOJI_RED_FLAG "\xf0\x9f\x9a\xa9" +#define ICON_EMOJI_ARROW_UP "\xe2\xac\x86" +#define ICON_EMOJI_ARROW_LEFT "\xe2\xac\x85" +#define ICON_EMOJI_ARROW_DOWN "\xe2\xac\x87" +#define ICON_EMOJI_ARROW_RIGHT "\xe2\x9e\xa1" +#define ICON_EMOJI_ARROW_RIGHT_CURVE_UP "\xe2\xa4\xb4" diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index f680033a5..97c9a31d8 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -741,13 +741,11 @@ void FullscreenUI::DoResume() void FullscreenUI::DoStartFile() { - auto callback = [](const std::string& path) { - if (!path.empty()) - DoStartPath(path); - }; - - OpenFileSelector(FSUI_ICONVSTR(ICON_EMOJI_OPTICAL_DISK, "Select Disc Image"), false, std::move(callback), - GetDiscImageFilters()); + OpenFileSelector(FSUI_ICONVSTR(ICON_EMOJI_OPTICAL_DISK, "Select Disc Image"), GetDiscImageFilters(), {}, + [](const std::string& path) { + if (!path.empty()) + DoStartPath(path); + }); } void FullscreenUI::DoStartBIOS() @@ -877,35 +875,34 @@ void FullscreenUI::DoToggleFastForward() void FullscreenUI::StartChangeDiscFromFile(bool return_to_game) { - auto callback = [return_to_game](const std::string& path) { - if (path.empty()) - { - if (return_to_game) - UnpauseForMenuClose(); - return; - } - - ConfirmWithSafetyCheck(FSUI_STR("change disc"), false, [path, return_to_game](bool result) { - if (result) + OpenFileSelector( + FSUI_ICONVSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), GetDiscImageFilters(), + std::string(Path::GetDirectory(VideoThread::GetGamePath())), [return_to_game](const std::string& path) { + if (path.empty()) { - if (!GameList::IsScannableFilename(path)) - { - OpenInfoMessageDialog(ICON_EMOJI_NO_ENTRY_SIGN, FSUI_STR("Error"), - fmt::format(FSUI_FSTR("{} is not a valid disc image."), Path::GetFileName(path))); - if (return_to_game) - UnpauseForMenuClose(); - } - else + if (return_to_game) + UnpauseForMenuClose(); + return; + } + + ConfirmWithSafetyCheck(FSUI_STR("change disc"), false, [path, return_to_game](bool result) { + if (result) { - Host::RunOnCoreThread([path]() { System::InsertMedia(path.c_str()); }); - ClosePauseMenuImmediately(); + if (!GameList::IsScannableFilename(path)) + { + OpenInfoMessageDialog(ICON_EMOJI_NO_ENTRY_SIGN, FSUI_STR("Error"), + fmt::format(FSUI_FSTR("{} is not a valid disc image."), Path::GetFileName(path))); + if (return_to_game) + UnpauseForMenuClose(); + } + else + { + Host::RunOnCoreThread([path]() { System::InsertMedia(path.c_str()); }); + ClosePauseMenuImmediately(); + } } - } + }); }); - }; - - OpenFileSelector(FSUI_ICONVSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), false, std::move(callback), - GetDiscImageFilters(), std::string(Path::GetDirectory(VideoThread::GetGamePath()))); // This can come from the core thread without the menu, so need to to trigger run idle. UpdateRunIdleState(); diff --git a/src/core/fullscreenui_game_list.cpp b/src/core/fullscreenui_game_list.cpp index 91db1aa3e..6518ab9db 100644 --- a/src/core/fullscreenui_game_list.cpp +++ b/src/core/fullscreenui_game_list.cpp @@ -86,38 +86,36 @@ void FullscreenUI::ClearGameListState() void FullscreenUI::DoSetCoverImage(std::string entry_path) { - OpenFileSelector( - FSUI_ICONVSTR(ICON_FA_IMAGE, "Set Cover Image"), false, - [entry_path = std::move(entry_path)](std::string path) { - if (path.empty()) - return; - - const auto lock = GameList::GetLock(); - const GameList::Entry* entry = GameList::GetEntryForPath(entry_path); - if (!entry) - return; - - std::string existing_path = GameList::GetCoverImagePathForEntry(entry); - std::string new_path = GameList::GetNewCoverImagePathForEntry(entry, path.c_str(), false); - if (!existing_path.empty()) - { - OpenConfirmMessageDialog( - ICON_EMOJI_WARNING, FSUI_ICONVSTR(ICON_FA_IMAGE, "Set Cover Image"), - FSUI_STR("A cover already exists for this game. Are you sure that you want to overwrite it?"), - [path = std::move(path), existing_path = std::move(existing_path), - new_path = std::move(new_path)](bool result) { - if (!result) - return; - - DoSetCoverImage(std::move(path), std::move(existing_path), std::move(new_path)); - }); - } - else - { - DoSetCoverImage(std::move(path), std::move(existing_path), std::move(new_path)); - } - }, - GetImageFilters(), EmuFolders::Covers); + OpenFileSelector(FSUI_ICONVSTR(ICON_FA_IMAGE, "Set Cover Image"), GetImageFilters(), EmuFolders::Covers, + [entry_path = std::move(entry_path)](std::string path) { + if (path.empty()) + return; + + const auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(entry_path); + if (!entry) + return; + + std::string existing_path = GameList::GetCoverImagePathForEntry(entry); + std::string new_path = GameList::GetNewCoverImagePathForEntry(entry, path.c_str(), false); + if (!existing_path.empty()) + { + OpenConfirmMessageDialog( + ICON_EMOJI_WARNING, FSUI_ICONVSTR(ICON_FA_IMAGE, "Set Cover Image"), + FSUI_STR("A cover already exists for this game. Are you sure that you want to overwrite it?"), + [path = std::move(path), existing_path = std::move(existing_path), + new_path = std::move(new_path)](bool result) { + if (!result) + return; + + DoSetCoverImage(std::move(path), std::move(existing_path), std::move(new_path)); + }); + } + else + { + DoSetCoverImage(std::move(path), std::move(existing_path), std::move(new_path)); + } + }); } void FullscreenUI::DoSetCoverImage(std::string source_path, std::string existing_path, std::string new_path) diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index ffec9e1f9..0e91938c5 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -1611,22 +1611,22 @@ void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, std::string_view ti { if (MenuButton(title, runtime_var)) { - OpenFileSelector(title, true, - [game_settings = IsEditingGameSettings(bsi), section = TinyString(section), - key = TinyString(key)](const std::string& dir) { - if (dir.empty()) - return; - - const auto lock = Core::GetSettingsLock(); - SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); - std::string relative_path(Path::MakeRelative(dir, EmuFolders::DataRoot)); - bsi->SetStringValue(section.c_str(), key.c_str(), relative_path.c_str()); - SetSettingsChanged(bsi); - - Host::RunOnCoreThread(&EmuFolders::Update); - if (key == "Covers") - RemoveCoverCacheEntry({}); - }); + OpenDirectorySelector(title, runtime_var, EmuFolders::GetDefaultPath(&runtime_var), + [game_settings = IsEditingGameSettings(bsi), section = TinyString(section), + key = TinyString(key)](const std::string& dir) { + if (dir.empty()) + return; + + const auto lock = Core::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + std::string relative_path(Path::MakeRelative(dir, EmuFolders::DataRoot)); + bsi->SetStringValue(section.c_str(), key.c_str(), relative_path.c_str()); + SetSettingsChanged(bsi); + + Host::RunOnCoreThread(&EmuFolders::Update); + if (key == "Covers") + RemoveCoverCacheEntry({}); + }); } } @@ -2630,19 +2630,20 @@ void FullscreenUI::DrawGameListSettingsPage() if (MenuButton(FSUI_ICONVSTR(ICON_FA_FOLDER_PLUS, "Add Search Directory"), FSUI_VSTR("Adds a new directory to the game search list."))) { - OpenFileSelector(FSUI_ICONVSTR(ICON_FA_FOLDER_PLUS, "Add Search Directory"), true, [](const std::string& dir) { - if (!dir.empty()) - { - const auto lock = Core::GetSettingsLock(); - SettingsInterface* bsi = Core::GetBaseSettingsLayer(); - - bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); - bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); - SetSettingsChanged(bsi); - PopulateGameListDirectoryCache(*bsi); - Host::RefreshGameListAsync(false); - } - }); + OpenDirectorySelector(FSUI_ICONVSTR(ICON_FA_FOLDER_PLUS, "Add Search Directory"), {}, {}, + [](const std::string& dir) { + if (!dir.empty()) + { + const auto lock = Core::GetSettingsLock(); + SettingsInterface* bsi = Core::GetBaseSettingsLayer(); + + bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); + bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); + SetSettingsChanged(bsi); + PopulateGameListDirectoryCache(*bsi); + Host::RefreshGameListAsync(false); + } + }); } for (const auto& it : s_settings_locals.game_list_directories_cache) @@ -3839,7 +3840,6 @@ void FullscreenUI::DrawMemoryCardSettingsPage() static constexpr const std::array path_keys = {"Card1Path", "Card2Path"}; SettingsInterface* bsi = GetEditingSettingsInterface(); - const bool game_settings = IsEditingGameSettings(bsi); BeginMenuButtons(); ResetFocusHere(); @@ -3915,7 +3915,7 @@ void FullscreenUI::DrawMemoryCardSettingsPage() } } - MenuHeading(FSUI_VSTR("Settings and Operations")); + MenuHeading(FSUI_VSTR("Save Locations")); DrawFolderSetting(bsi, FSUI_ICONVSTR(ICON_FA_FOLDER_OPEN, "Memory Card Directory"), "MemoryCards", "Directory", EmuFolders::MemoryCards); @@ -3923,12 +3923,7 @@ void FullscreenUI::DrawMemoryCardSettingsPage() DrawFolderSetting(bsi, FSUI_ICONVSTR(ICON_FA_FOLDER_OPEN, "Save State Directory"), "Folders", "SaveStates", EmuFolders::SaveStates); - if (!game_settings && MenuButton(FSUI_ICONVSTR(ICON_FA_ARROW_ROTATE_LEFT, "Reset Memory Card Directory"), - FSUI_VSTR("Resets memory card directory to default (user directory)."))) - { - bsi->SetStringValue("MemoryCards", "Directory", "memcards"); - SetSettingsChanged(bsi); - } + MenuHeading(FSUI_VSTR("Settings")); DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_SHARE_NODES, "Use Single Card For Multi-Disc Games"), FSUI_VSTR("When playing a multi-disc game and using per-game (title) memory cards, " @@ -4894,18 +4889,16 @@ void FullscreenUI::DrawPostProcessingSettingsPage() if (MenuButton(FSUI_ICONVSTR(ICON_FA_IMAGE, "Image Path"), GetEffectiveTinyStringSetting(bsi, "BorderOverlay", "ImagePath", ""))) { - OpenFileSelector( - FSUI_ICONVSTR(ICON_FA_IMAGE, "Image Path"), false, - [game_settings = IsEditingGameSettings(bsi)](const std::string& path) { - if (path.empty()) - return; + OpenFileSelector(FSUI_ICONVSTR(ICON_FA_IMAGE, "Image Path"), GetImageFilters(), {}, + [game_settings = IsEditingGameSettings(bsi)](const std::string& path) { + if (path.empty()) + return; - SettingsInterface* const bsi = GetEditingSettingsInterface(game_settings); - bsi->SetStringValue("BorderOverlay", "ImagePath", path.c_str()); - SetSettingsChanged(bsi); - queue_reload(); - }, - GetImageFilters()); + SettingsInterface* const bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("BorderOverlay", "ImagePath", path.c_str()); + SetSettingsChanged(bsi); + queue_reload(); + }); } reload_pending |= DrawIntRectSetting( diff --git a/src/core/fullscreenui_strings.h b/src/core/fullscreenui_strings.h index 056343a50..09b8be7d7 100644 --- a/src/core/fullscreenui_strings.h +++ b/src/core/fullscreenui_strings.h @@ -86,8 +86,6 @@ TRANSLATE_NOOP("FullscreenUI", "9 Frames"); TRANSLATE_NOOP("FullscreenUI", "90% [54 FPS (NTSC) / 45 FPS (PAL)]"); TRANSLATE_NOOP("FullscreenUI", "900% [540 FPS (NTSC) / 450 FPS (PAL)]"); TRANSLATE_NOOP("FullscreenUI", "9x (for 4K)"); -TRANSLATE_NOOP("FullscreenUI", ""); -TRANSLATE_NOOP("FullscreenUI", ""); TRANSLATE_NOOP("FullscreenUI", "A cover already exists for this game. Are you sure that you want to overwrite it?"); TRANSLATE_NOOP("FullscreenUI", "A resume save state was found for this game, but it is corrupted and cannot be loaded:\n\n{}\n\nDo you want to delete the save state and boot the game anyway?"); TRANSLATE_NOOP("FullscreenUI", "AMOLED"); @@ -634,12 +632,11 @@ TRANSLATE_NOOP("FullscreenUI", "Rendering"); TRANSLATE_NOOP("FullscreenUI", "Replaces these settings with a previously saved controller preset."); TRANSLATE_NOOP("FullscreenUI", "Rescan All Games"); TRANSLATE_NOOP("FullscreenUI", "Reset Controller Settings"); -TRANSLATE_NOOP("FullscreenUI", "Reset Memory Card Directory"); TRANSLATE_NOOP("FullscreenUI", "Reset Play Time"); TRANSLATE_NOOP("FullscreenUI", "Reset Settings"); +TRANSLATE_NOOP("FullscreenUI", "Reset To Default"); TRANSLATE_NOOP("FullscreenUI", "Resets all configuration to defaults (including bindings)."); TRANSLATE_NOOP("FullscreenUI", "Resets all settings to the defaults."); -TRANSLATE_NOOP("FullscreenUI", "Resets memory card directory to default (user directory)."); TRANSLATE_NOOP("FullscreenUI", "Restart Game"); TRANSLATE_NOOP("FullscreenUI", "Restore Defaults"); TRANSLATE_NOOP("FullscreenUI", "Resume Game"); @@ -665,6 +662,7 @@ TRANSLATE_NOOP("FullscreenUI", "SDL DualSense Player LED"); TRANSLATE_NOOP("FullscreenUI", "SDL DualShock 4 / DualSense Enhanced Mode"); TRANSLATE_NOOP("FullscreenUI", "Safe Mode"); TRANSLATE_NOOP("FullscreenUI", "Save Controller Preset"); +TRANSLATE_NOOP("FullscreenUI", "Save Locations"); TRANSLATE_NOOP("FullscreenUI", "Save Preset"); TRANSLATE_NOOP("FullscreenUI", "Save Screenshot"); TRANSLATE_NOOP("FullscreenUI", "Save State"); @@ -735,7 +733,6 @@ TRANSLATE_NOOP("FullscreenUI", "Sets the verbosity of messages logged. Higher le TRANSLATE_NOOP("FullscreenUI", "Sets which sort of memory card image will be used for slot {}."); TRANSLATE_NOOP("FullscreenUI", "Setting {} binding {}."); TRANSLATE_NOOP("FullscreenUI", "Settings"); -TRANSLATE_NOOP("FullscreenUI", "Settings and Operations"); TRANSLATE_NOOP("FullscreenUI", "Settings reset to default."); TRANSLATE_NOOP("FullscreenUI", "Shader {} added as stage {}."); TRANSLATE_NOOP("FullscreenUI", "Shared Card Name"); @@ -865,6 +862,7 @@ TRANSLATE_NOOP("FullscreenUI", "Use Separate Disc Settings"); TRANSLATE_NOOP("FullscreenUI", "Use Single Card For Multi-Disc Games"); TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer (Low VRAM Mode)"); TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For Readbacks"); +TRANSLATE_NOOP("FullscreenUI", "Use This Directory"); TRANSLATE_NOOP("FullscreenUI", "User Name"); TRANSLATE_NOOP("FullscreenUI", "Uses OpenGL ES even when desktop OpenGL is supported. May improve performance on some SBC drivers."); TRANSLATE_NOOP("FullscreenUI", "Uses PGXP for all instructions, not just memory operations."); diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index 7a9a83ca1..39d136bb9 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -271,7 +271,7 @@ public: ~FileSelectorDialog(); void Open(std::string_view title, FileSelectorCallback callback, FileSelectorFilters filters, - std::string initial_directory, bool select_directory); + std::string initial_directory); void ClearState(); void Draw(); @@ -298,10 +298,50 @@ private: std::string m_current_directory; std::vector m_items; - std::vector m_filters; + FileSelectorFilters m_filters; + FileSelectorCallback m_callback; + + bool m_directory_changed = false; + bool m_first_item_is_parent_directory = false; +}; + +class DirectorySelectorDialog : public PopupDialog +{ +public: + DirectorySelectorDialog(); + ~DirectorySelectorDialog(); + + void Open(std::string_view title, DirectorySelectorCallback callback, std::string initial_directory, + std::string default_directory); + void ClearState(); + + void Draw(); + +private: + struct Item + { + Item() = default; + Item(std::string display_name_, std::string full_path_, bool is_file_); + Item(const Item&) = default; + Item(Item&&) = default; + ~Item() = default; + + Item& operator=(const Item&) = default; + Item& operator=(Item&&) = default; + + std::string display_name; + std::string full_path; + bool is_file; + }; + + void PopulateItems(); + void SetDirectory(std::string dir); + + std::string m_current_directory; + std::vector m_items; + std::string m_default_directory; FileSelectorCallback m_callback; - bool m_is_directory = false; bool m_directory_changed = false; bool m_first_item_is_parent_directory = false; }; @@ -435,9 +475,11 @@ struct WidgetsState ImAnimatedVec2 menu_button_frame_min_animated; ImAnimatedVec2 menu_button_frame_max_animated; + // TODO: Make these dynamic rather than global state. ChoiceDialog choice_dialog; DropdownDialog dropdown_dialog; FileSelectorDialog file_selector_dialog; + DirectorySelectorDialog directory_selector_dialog; InputStringDialog input_string_dialog; FixedPopupDialog fixed_popup_dialog; ProgressDialog progress_dialog; @@ -1648,6 +1690,7 @@ void FullscreenUI::EndLayout() s_state.choice_dialog.Draw(); s_state.dropdown_dialog.Draw(); s_state.file_selector_dialog.Draw(); + s_state.directory_selector_dialog.Draw(); s_state.input_string_dialog.Draw(); s_state.progress_dialog.Draw(); s_state.message_dialog.Draw(); @@ -3218,6 +3261,24 @@ void FullscreenUI::MenuHeading(std::string_view title, bool draw_line /*= true*/ ImGui::Dummy(ImVec2(0.0f, total_height)); } +void FullscreenUI::MenuSeparator() +{ + const float line_thickness = LayoutScale(1.0f); + const float line_padding = LayoutScale(5.0f); + const float avail_width = MenuButtonBounds::CalcAvailWidth(); + + const ImGuiWindow* const window = ImGui::GetCurrentWindowRead(); + const ImGuiStyle& style = ImGui::GetStyle(); + const ImVec2 pos = ImVec2(window->DC.CursorPos.x + style.FramePadding.x, window->DC.CursorPos.y); + const ImVec2 line_start = ImVec2(pos.x, pos.y + line_padding); + const ImVec2 line_end = ImVec2(line_start.x + avail_width, line_start.y); + + window->DrawList->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), line_thickness); + + const float total_height = line_thickness + style.FramePadding.y; + ImGui::Dummy(ImVec2(0.0f, total_height)); +} + bool FullscreenUI::MenuHeadingButton(std::string_view title, std::string_view value /*= {}*/, float font_size /*= UIStyle.LargeFontSize */, bool enabled /*= true*/, bool draw_line /*= true*/) @@ -4802,8 +4863,7 @@ FullscreenUI::FileSelectorDialog::FileSelectorDialog() = default; FullscreenUI::FileSelectorDialog::~FileSelectorDialog() = default; void FullscreenUI::FileSelectorDialog::Open(std::string_view title, FileSelectorCallback callback, - FileSelectorFilters filters, std::string initial_directory, - bool select_directory) + FileSelectorFilters filters, std::string initial_directory) { if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) initial_directory = FileSystem::GetWorkingDirectory(); @@ -4811,7 +4871,6 @@ void FullscreenUI::FileSelectorDialog::Open(std::string_view title, FileSelector SetTitleAndOpen(fmt::format("{}##file_selector_dialog", title)); m_callback = std::move(callback); m_filters = std::move(filters); - m_is_directory = select_directory; SetDirectory(std::move(initial_directory)); } @@ -4820,7 +4879,6 @@ void FullscreenUI::FileSelectorDialog::ClearState() PopupDialog::ClearState(); m_callback = {}; m_filters = {}; - m_is_directory = false; m_directory_changed = false; } @@ -4846,9 +4904,8 @@ void FullscreenUI::FileSelectorDialog::PopulateItems() { FileSystem::FindResultsArray results; FileSystem::FindFiles(m_current_directory.c_str(), "*", - (m_is_directory ? 0 : FILESYSTEM_FIND_FILES) | FILESYSTEM_FIND_FOLDERS | - FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS | - FILESYSTEM_FIND_SORT_BY_NAME, + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | + FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME, &results); // Ensure we only go back to the root list once we've gone up from the root of that drive. @@ -4863,8 +4920,8 @@ void FullscreenUI::FileSelectorDialog::PopulateItems() parent_path.push_back(FS_OSPATH_SEPARATOR_CHARACTER); } - m_items.emplace_back(fmt::format(ICON_EMOJI_FILE_FOLDER_OPEN " {}", FSUI_VSTR("")), - std::move(parent_path), false); + m_items.emplace_back(fmt::format(ICON_EMOJI_ARROW_UP " {}", FSUI_VSTR("Parent Directory")), std::move(parent_path), + false); m_first_item_is_parent_directory = true; for (const FILESYSTEM_FIND_DATA& fd : results) @@ -4931,20 +4988,10 @@ void FullscreenUI::FileSelectorDialog::Draw() BeginMenuButtons(); Item* selected = nullptr; - bool directory_selected = false; if (!m_current_directory.empty()) MenuButtonWithoutSummary(SmallString::from_format(ICON_FA_FOLDER_OPEN " {}", m_current_directory), false); - if (m_is_directory && !m_current_directory.empty()) - { - if (MenuButtonWithoutSummary( - SmallString::from_format(ICON_EMOJI_FILE_FOLDER_OPEN " {}", FSUI_VSTR("")))) - { - directory_selected = true; - } - } - for (Item& item : m_items) { if (MenuButtonWithoutSummary(item.display_name)) @@ -4985,13 +5032,6 @@ void FullscreenUI::FileSelectorDialog::Draw() [this, dir = std::move(selected->full_path)]() mutable { SetDirectory(std::move(dir)); }); } } - else if (directory_selected) - { - std::string path = std::exchange(m_current_directory, std::string()); - const FileSelectorCallback callback = std::exchange(m_callback, FileSelectorCallback()); - StartClose(); - callback(std::move(path)); - } else { if (ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadContextMenu, false)) @@ -5010,11 +5050,10 @@ bool FullscreenUI::IsFileSelectorOpen() return s_state.file_selector_dialog.IsOpen(); } -void FullscreenUI::OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, - FileSelectorFilters filters, std::string initial_directory) +void FullscreenUI::OpenFileSelector(std::string_view title, FileSelectorFilters filters, std::string initial_directory, + FileSelectorCallback callback) { - s_state.file_selector_dialog.Open(title, std::move(callback), std::move(filters), std::move(initial_directory), - select_directory); + s_state.file_selector_dialog.Open(title, std::move(callback), std::move(filters), std::move(initial_directory)); } void FullscreenUI::CloseFileSelector() @@ -5022,6 +5061,211 @@ void FullscreenUI::CloseFileSelector() s_state.file_selector_dialog.StartClose(); } +FullscreenUI::DirectorySelectorDialog::DirectorySelectorDialog() = default; + +FullscreenUI::DirectorySelectorDialog::~DirectorySelectorDialog() = default; + +void FullscreenUI::DirectorySelectorDialog::Open(std::string_view title, DirectorySelectorCallback callback, + std::string initial_directory, std::string default_directory) +{ + if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) + initial_directory = FileSystem::GetWorkingDirectory(); + + SetTitleAndOpen(fmt::format("{}##file_selector_dialog", title)); + m_default_directory = std::move(default_directory); + m_callback = std::move(callback); + SetDirectory(std::move(initial_directory)); +} + +void FullscreenUI::DirectorySelectorDialog::ClearState() +{ + PopupDialog::ClearState(); + m_callback = {}; + m_default_directory = {}; + m_directory_changed = false; +} + +FullscreenUI::DirectorySelectorDialog::Item::Item(std::string display_name_, std::string full_path_, bool is_file_) + : display_name(std::move(display_name_)), full_path(std::move(full_path_)), is_file(is_file_) +{ +} + +void FullscreenUI::DirectorySelectorDialog::PopulateItems() +{ + m_items.clear(); + m_first_item_is_parent_directory = false; + + if (m_current_directory.empty()) + { + for (std::string& root_path : FileSystem::GetRootDirectoryList()) + { + std::string label = fmt::format(ICON_EMOJI_FILE_FOLDER " {}", root_path); + m_items.emplace_back(std::move(label), std::move(root_path), false); + } + } + else + { + FileSystem::FindResultsArray results; + FileSystem::FindFiles(m_current_directory.c_str(), "*", + FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS | + FILESYSTEM_FIND_SORT_BY_NAME, + &results); + + // Ensure we only go back to the root list once we've gone up from the root of that drive. + std::string parent_path; + std::string::size_type sep_pos = m_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER); + if (sep_pos != std::string::npos && sep_pos != (m_current_directory.size() - 1)) + { + parent_path = Path::Canonicalize(m_current_directory.substr(0, sep_pos)); + + // Ensure that the root directory has a trailing backslash. + if (parent_path.find(FS_OSPATH_SEPARATOR_CHARACTER) == std::string::npos) + parent_path.push_back(FS_OSPATH_SEPARATOR_CHARACTER); + } + + m_items.emplace_back(fmt::format(ICON_EMOJI_ARROW_UP " {}", FSUI_VSTR("Parent Directory")), std::move(parent_path), + false); + m_first_item_is_parent_directory = true; + + for (const FILESYSTEM_FIND_DATA& fd : results) + { + std::string full_path = Path::Combine(m_current_directory, fd.FileName); + std::string title = fmt::format(ICON_EMOJI_FILE_FOLDER " {}", fd.FileName); + m_items.emplace_back(std::move(title), std::move(full_path), false); + } + } +} + +void FullscreenUI::DirectorySelectorDialog::SetDirectory(std::string dir) +{ + // Ensure at least one slash always exists. + while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER && + dir.find(FS_OSPATH_SEPARATOR_CHARACTER) != (dir.size() - 1)) + { + dir.pop_back(); + } + + m_current_directory = std::move(dir); + m_directory_changed = true; + PopulateItems(); +} + +void FullscreenUI::DirectorySelectorDialog::Draw() +{ + if (!IsOpen()) + return; + + if (!BeginRender(LayoutScale(10.0f), LayoutScale(20.0f), LayoutScale(1000.0f, 650.0f))) + { + const DirectorySelectorCallback callback = std::move(m_callback); + ClearState(); + if (callback) + callback(std::string()); + return; + } + + if (m_directory_changed) + { + m_directory_changed = false; + ImGui::SetScrollY(0.0f); + QueueResetFocus(FocusResetType::Other); + } + + ResetFocusHere(); + BeginMenuButtons(); + + Item* selected = nullptr; + std::string selected_directory; + + if (!m_current_directory.empty()) + { + MenuButtonWithoutSummary(SmallString::from_format(ICON_FA_FOLDER_OPEN " {}", m_current_directory), false); + + if (MenuButtonWithoutSummary(FSUI_ICONVSTR(ICON_EMOJI_CHECKMARK_BUTTON, "Use This Directory"))) + selected_directory = m_current_directory; + } + + if (!m_default_directory.empty()) + { + if (MenuButtonWithoutSummary(FSUI_ICONVSTR(ICON_EMOJI_REFRESH, "Reset To Default"))) + selected_directory = m_default_directory; + } + + bool needs_separator = + (!m_current_directory.empty() || !m_default_directory.empty() || m_first_item_is_parent_directory); + for (size_t i = 0; i < m_items.size(); i++) + { + if (needs_separator && (i > 0 || !m_first_item_is_parent_directory)) + { + needs_separator = false; + MenuSeparator(); + } + + Item& item = m_items[i]; + if (MenuButtonWithoutSummary(item.display_name)) + selected = &item; + } + + EndMenuButtons(); + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Parent Directory")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))}); + } + else + { + SetFullscreenFooterText( + std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BACKSPACE, FSUI_VSTR("Parent Directory")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))}); + } + + EndRender(); + + if (selected) + { + BeginTransition(DEFAULT_TRANSITION_TIME, + [this, dir = std::move(selected->full_path)]() mutable { SetDirectory(std::move(dir)); }); + } + else if (!selected_directory.empty()) + { + const DirectorySelectorCallback callback = std::exchange(m_callback, DirectorySelectorCallback()); + StartClose(); + callback(std::move(selected_directory)); + } + else + { + if (ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadContextMenu, false)) + { + if (!m_items.empty() && m_first_item_is_parent_directory) + { + BeginTransition(DEFAULT_TRANSITION_TIME, + [this, dir = std::move(m_items.front().full_path)]() mutable { SetDirectory(std::move(dir)); }); + } + } + } +} + +bool FullscreenUI::IsDirectorySelectorOpen() +{ + return s_state.directory_selector_dialog.IsOpen(); +} + +void FullscreenUI::OpenDirectorySelector(std::string_view title, std::string initial_directory, + std::string default_directory, DirectorySelectorCallback callback) +{ + s_state.directory_selector_dialog.Open(title, std::move(callback), std::move(initial_directory), + std::move(default_directory)); +} + +void FullscreenUI::CloseDirectorySelector() +{ + s_state.directory_selector_dialog.StartClose(); +} + FullscreenUI::ChoiceDialog::ChoiceDialog() = default; FullscreenUI::ChoiceDialog::~ChoiceDialog() = default; diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index 4d15f5c9a..e208197d6 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -424,6 +424,7 @@ void RenderAutoLabelText(ImDrawList* draw_list, ImFont* font, float font_size, f void TextAlignedMultiLine(float align_x, const char* text, const char* text_end = nullptr, float wrap_width = -1.0f); void TextUnformatted(std::string_view text); void MenuHeading(std::string_view title, bool draw_line = true); +void MenuSeparator(); bool MenuHeadingButton(std::string_view title, std::string_view value = {}, float font_size = UIStyle.LargeFontSize, bool enabled = true, bool draw_line = true); bool MenuButton(std::string_view title, std::string_view summary, bool enabled = true, @@ -540,11 +541,16 @@ bool AreAnyWidgetsDialogInteractable(); using FileSelectorCallback = std::function; using FileSelectorFilters = std::vector; bool IsFileSelectorOpen(); -void OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, - FileSelectorFilters filters = FileSelectorFilters(), - std::string initial_directory = std::string()); +void OpenFileSelector(std::string_view title, FileSelectorFilters filters, std::string initial_directory, + FileSelectorCallback callback); void CloseFileSelector(); +using DirectorySelectorCallback = std::function; +bool IsDirectorySelectorOpen(); +void OpenDirectorySelector(std::string_view title, std::string initial_directory, std::string default_directory, + DirectorySelectorCallback callback); +void CloseDirectorySelector(); + using ChoiceDialogCallback = std::function; using ChoiceDialogOptions = std::vector>; bool IsChoiceDialogOpen(); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 16c0301d5..26f04a67c 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -2775,6 +2775,10 @@ const char* Settings::GetPIODeviceTypeModeDisplayName(PIODeviceType type) namespace EmuFolders { +static void EnsureFolderExists(const std::string& path); +static std::string LoadPathFromSettings(SettingsInterface& si, const std::string& root, const char* section, + const char* name, const char* def); + std::string AppRoot; std::string DataRoot; std::string Bios; @@ -2795,10 +2799,33 @@ std::string Textures; std::string UserResources; std::string Videos; -static void EnsureFolderExists(const std::string& path); - } // namespace EmuFolders +std::string EmuFolders::GetDefaultPath(const std::string* ref_folder) +{ + // clang-format off + std::string_view subdir; + if (ref_folder == &Bios) subdir = "bios"; + else if (ref_folder == &Cache) subdir = "cache"; + else if (ref_folder == &Cheats) subdir = "cheats"; + else if (ref_folder == &Covers) subdir = "covers"; + else if (ref_folder == &GameIcons) subdir = "gameicons"; + else if (ref_folder == &GameSettings) subdir = "gamesettings"; + else if (ref_folder == &InputProfiles) subdir = "inputprofiles"; + else if (ref_folder == &MemoryCards) subdir = "memcards"; + else if (ref_folder == &Patches) subdir = "patches"; + else if (ref_folder == &SaveStates) subdir = "savestates"; + else if (ref_folder == &Screenshots) subdir = "screenshots"; + else if (ref_folder == &Shaders) subdir = "shaders"; + else if (ref_folder == &Subchannels) subdir = "subchannels"; + else if (ref_folder == &Textures) subdir = "textures"; + else if (ref_folder == &UserResources) subdir = "resources"; + else if (ref_folder == &Videos) subdir = "videos"; + // clang-format on + + return Path::Combine(DataRoot, subdir); +} + void EmuFolders::SetDefaults() { Bios = Path::Combine(DataRoot, "bios"); @@ -2819,7 +2846,7 @@ void EmuFolders::SetDefaults() Videos = Path::Combine(DataRoot, "videos"); } -static std::string LoadPathFromSettings(SettingsInterface& si, const std::string& root, const char* section, +std::string EmuFolders::LoadPathFromSettings(SettingsInterface& si, const std::string& root, const char* section, const char* name, const char* def) { std::string value = si.GetStringValue(section, name, def); diff --git a/src/core/settings.h b/src/core/settings.h index a37760b8e..a27c1c620 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -713,6 +713,9 @@ void EnsureFoldersExist(); void LoadConfig(SettingsInterface& si); void Save(SettingsInterface& si); +// Returns the default path for the given settings key. +std::string GetDefaultPath(const std::string* ref_folder); + /// Updates the variables in the EmuFolders namespace, reloading subsystems if needed. void Update();