From 2d31bf5685c939d7612a2cc30d2173ff45bcf5f4 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 30 Mar 2025 12:53:18 +1000 Subject: [PATCH] FullscreenUI: Convert input binding into a PopupDialog --- src/core/fullscreen_ui.cpp | 204 ++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 93 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 94b4c393c..80300a8ca 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -293,8 +293,6 @@ static void DoSetCoverImage(std::string source_path, std::string existing_path, // Settings ////////////////////////////////////////////////////////////////////////// -static constexpr double INPUT_BINDING_TIMEOUT_SECONDS = 5.0; - static void SwitchToSettings(); static bool SwitchToGameSettings(); static void SwitchToGameSettings(const GameList::Entry* entry); @@ -409,15 +407,11 @@ static void PopulateGraphicsAdapterList(); static void PopulateGameListDirectoryCache(SettingsInterface* si); static void PopulatePatchesAndCheatsList(); static void PopulatePostProcessingChain(SettingsInterface* si, const char* section); -static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section, - std::string_view key, std::string_view display_name); static void BeginVibrationMotorBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, const char* key, std::string_view display_name); -static void DrawInputBindingWindow(); static void DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, const char* name, const char* display_name, const char* icon_name, bool show_type = true); -static void ClearInputBindingVariables(); static void StartAutomaticBindingForPort(u32 port); static void StartClearBindingsForPort(u32 port); @@ -507,6 +501,30 @@ static constexpr std::string_view ABOUT_DIALOG_NAME = "##about_duckstation"; namespace { +class InputBindingDialog : public ImGuiFullscreen::PopupDialog +{ +public: + InputBindingDialog(); + ~InputBindingDialog(); + + void Draw(); + void ClearState(); + + void Start(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section, std::string_view key, + std::string_view display_name); + +private: + static constexpr float INPUT_BINDING_TIMEOUT_SECONDS = 5.0f; + + std::string m_binding_section; + std::string m_binding_key; + std::string m_display_name; + std::vector m_new_bindings; + std::vector>> m_value_ranges; + float m_time_remaining = 0.0f; + InputBindingInfo::Type m_binding_type = InputBindingInfo::Type::Unknown; +}; + struct ALIGN_TO_CACHE_LINE UIState { // Main @@ -554,14 +572,8 @@ struct ALIGN_TO_CACHE_LINE UIState std::vector hotkey_list_cache; std::atomic_bool settings_changed{false}; std::atomic_bool game_settings_changed{false}; - InputBindingInfo::Type input_binding_type = InputBindingInfo::Type::Unknown; - std::string input_binding_section; - std::string input_binding_key; - std::string input_binding_display_name; - std::vector input_binding_new_bindings; - std::vector>> input_binding_value_ranges; - Timer::Value input_binding_start_time = 0; bool controller_macro_expanded[NUM_CONTROLLER_AND_CARD_PORTS][InputManager::NUM_MACRO_BUTTONS_PER_CONTROLLER] = {}; + InputBindingDialog input_binding_dialog; // Save State List std::vector save_state_selector_slots; @@ -757,11 +769,10 @@ bool FullscreenUI::HasActiveWindow() bool FullscreenUI::AreAnyDialogsOpen() { - return (s_state.input_binding_type != InputBindingInfo::Type::Unknown || - ImGuiFullscreen::IsAnyFixedPopupDialogOpen() || ImGuiFullscreen::IsChoiceDialogOpen() || - ImGuiFullscreen::IsInputDialogOpen() || ImGuiFullscreen::IsFileSelectorOpen() || - ImGuiFullscreen::IsMessageBoxDialogOpen() || ImGuiFullscreen::HasToast() || - ImGuiFullscreen::HasAnyNotifications()); + return (s_state.input_binding_dialog.IsOpen() || ImGuiFullscreen::IsAnyFixedPopupDialogOpen() || + ImGuiFullscreen::IsChoiceDialogOpen() || ImGuiFullscreen::IsInputDialogOpen() || + ImGuiFullscreen::IsFileSelectorOpen() || ImGuiFullscreen::IsMessageBoxDialogOpen() || + ImGuiFullscreen::HasToast() || ImGuiFullscreen::HasAnyNotifications()); } void FullscreenUI::CheckForConfigChanges(const GPUSettings& old_settings) @@ -962,7 +973,7 @@ void FullscreenUI::Shutdown(bool clear_state) s_state.was_paused_on_quick_menu_open = false; Achievements::ClearUIState(); - ClearInputBindingVariables(); + s_state.input_binding_dialog.ClearState(); ClearSaveStateEntryList(); s_state.icon_image_map.clear(); s_state.cover_image_map.clear(); @@ -1055,8 +1066,8 @@ void FullscreenUI::Render() DrawAboutWindow(); else if (IsFixedPopupDialogOpen(RESUME_STATE_SELECTOR_DIALOG_NAME)) DrawResumeStateSelector(); - else if (s_state.input_binding_type != InputBindingInfo::Type::Unknown) - DrawInputBindingWindow(); + + s_state.input_binding_dialog.Draw(); ImGuiFullscreen::EndLayout(); @@ -2331,7 +2342,7 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingIn if (type == InputBindingInfo::Type::Motor) BeginVibrationMotorBinding(bsi, type, section, name, display_name); else - BeginInputBinding(bsi, type, section, name, display_name); + s_state.input_binding_dialog.Start(bsi, type, section, name, display_name); } else if (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false)) { @@ -2341,61 +2352,50 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingIn } } -void FullscreenUI::ClearInputBindingVariables() -{ - s_state.input_binding_type = InputBindingInfo::Type::Unknown; - s_state.input_binding_section = {}; - s_state.input_binding_key = {}; - s_state.input_binding_display_name = {}; - s_state.input_binding_new_bindings = {}; - s_state.input_binding_value_ranges = {}; +FullscreenUI::InputBindingDialog::InputBindingDialog() = default; - if (IsFixedPopupDialogOpen(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding"))) - CloseFixedPopupDialogImmediately(); -} +FullscreenUI::InputBindingDialog::~InputBindingDialog() = default; -void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section, - std::string_view key, std::string_view display_name) +void FullscreenUI::InputBindingDialog::Start(SettingsInterface* bsi, InputBindingInfo::Type type, + std::string_view section, std::string_view key, + std::string_view display_name) { - if (s_state.input_binding_type != InputBindingInfo::Type::Unknown) - { + if (m_binding_type != InputBindingInfo::Type::Unknown) InputManager::RemoveHook(); - ClearInputBindingVariables(); - } - s_state.input_binding_type = type; - s_state.input_binding_section = section; - s_state.input_binding_key = key; - s_state.input_binding_display_name = display_name; - s_state.input_binding_new_bindings = {}; - s_state.input_binding_value_ranges = {}; - s_state.input_binding_start_time = Timer::GetCurrentValue(); - OpenFixedPopupDialog(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding")); + m_binding_type = type; + m_binding_section = section; + m_binding_key = key; + m_display_name = display_name; + m_new_bindings = {}; + m_value_ranges = {}; + m_time_remaining = INPUT_BINDING_TIMEOUT_SECONDS; + SetTitleAndOpen(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding")); const bool game_settings = IsEditingGameSettings(bsi); - InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { - // shouldn't happen, just in case - if (s_state.input_binding_type == InputBindingInfo::Type::Unknown) - return InputInterceptHook::CallbackResult::RemoveHookAndContinueProcessingEvent; - + InputManager::SetHook([this, game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { // holding the settings lock here will protect the input binding list auto lock = Host::GetSettingsLock(); + // shouldn't happen, just in case + if (m_binding_type == InputBindingInfo::Type::Unknown) + return InputInterceptHook::CallbackResult::RemoveHookAndContinueProcessingEvent; + float initial_value = value; float min_value = value; InputInterceptHook::CallbackResult default_action = InputInterceptHook::CallbackResult::StopProcessingEvent; - const auto it = std::find_if(s_state.input_binding_value_ranges.begin(), s_state.input_binding_value_ranges.end(), + const auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), [key](const auto& it) { return it.first.bits == key.bits; }); - if (it != s_state.input_binding_value_ranges.end()) + if (it != m_value_ranges.end()) { initial_value = it->second.first; min_value = it->second.second = std::min(it->second.second, value); } else { - s_state.input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); // forward the event to imgui if it's a new key and a release, because this is what triggered the binding to // start if we don't do this, imgui thinks the activate button is held down @@ -2407,7 +2407,7 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && std::abs(initial_value) > 0.5f); - for (InputBindingKey& other_key : s_state.input_binding_new_bindings) + for (InputBindingKey& other_key : m_new_bindings) { // if this key is in our new binding list, it's a "release", and we're done if (other_key.MaskDirection() == key.MaskDirection()) @@ -2421,13 +2421,13 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); const std::string new_binding(InputManager::ConvertInputBindingKeysToString( - s_state.input_binding_type, s_state.input_binding_new_bindings.data(), - s_state.input_binding_new_bindings.size())); - bsi->SetStringValue(s_state.input_binding_section.c_str(), s_state.input_binding_key.c_str(), - new_binding.c_str()); + m_binding_type, m_new_bindings.data(), m_new_bindings.size())); + bsi->SetStringValue(m_binding_section.c_str(), m_binding_key.c_str(), new_binding.c_str()); SetSettingsChanged(bsi); - GPUThread::RunOnThread(&CloseFixedPopupDialog); + // don't try to process any more + m_binding_type = InputBindingInfo::Type::Unknown; + GPUThread::RunOnThread([this]() { StartClose(); }); return InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent; } @@ -2443,13 +2443,62 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T InputBindingKey key_to_add = key; key_to_add.modifier = (value < 0.0f) ? InputModifier::Negate : InputModifier::None; key_to_add.invert = reverse_threshold; - s_state.input_binding_new_bindings.push_back(key_to_add); + m_new_bindings.push_back(key_to_add); } return default_action; }); } +void FullscreenUI::InputBindingDialog::ClearState() +{ + PopupDialog::ClearState(); + + if (m_binding_type != InputBindingInfo::Type::Unknown) + InputManager::RemoveHook(); + + m_binding_type = InputBindingInfo::Type::Unknown; + m_binding_section = {}; + m_binding_key = {}; + m_display_name = {}; + m_new_bindings = {}; + m_value_ranges = {}; +} + +void FullscreenUI::InputBindingDialog::Draw() +{ + if (!IsOpen()) + return; + + if (m_time_remaining > 0.0f) + { + m_time_remaining -= ImGui::GetIO().DeltaTime; + if (m_time_remaining <= 0.0f) + { + // allow the dialog to fade out, but stop receiving any more events + m_time_remaining = 0.0f; + m_binding_type = InputBindingInfo::Type::Unknown; + InputManager::RemoveHook(); + StartClose(); + } + } + + if (!BeginRender(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 0.0f))) + { + ClearState(); + return; + } + + ImGui::TextWrapped( + "%s", SmallString::from_format(FSUI_FSTR("Setting {} binding {}."), m_binding_section, m_display_name).c_str()); + ImGui::TextUnformatted(FSUI_CSTR("Push a controller button or axis now.")); + ImGui::NewLine(); + ImGui::TextUnformatted(SmallString::from_format(FSUI_FSTR("Timing out in {:.0f} seconds..."), m_time_remaining)); + + EndRender(); +} + void FullscreenUI::BeginVibrationMotorBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, const char* key, std::string_view display_name) { @@ -2497,37 +2546,6 @@ void FullscreenUI::BeginVibrationMotorBinding(SettingsInterface* bsi, InputBindi }); } -void FullscreenUI::DrawInputBindingWindow() -{ - DebugAssert(s_state.input_binding_type != InputBindingInfo::Type::Unknown); - - const double time_remaining = - INPUT_BINDING_TIMEOUT_SECONDS - - Timer::ConvertValueToSeconds(Timer::GetCurrentValue() - s_state.input_binding_start_time); - if (time_remaining <= 0.0) - { - InputManager::RemoveHook(); - CloseFixedPopupDialog(); - } - - if (!BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), - LayoutScale(500.0f, 0.0f))) - { - InputManager::RemoveHook(); - ClearInputBindingVariables(); - return; - } - - ImGui::TextWrapped("%s", SmallString::from_format(FSUI_FSTR("Setting {} binding {}."), s_state.input_binding_section, - s_state.input_binding_display_name) - .c_str()); - ImGui::TextUnformatted(FSUI_CSTR("Push a controller button or axis now.")); - ImGui::NewLine(); - ImGui::TextUnformatted(SmallString::from_format(FSUI_FSTR("Timing out in {:.0f} seconds..."), time_remaining)); - - EndFixedPopupDialog(); -} - bool FullscreenUI::DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, bool default_value, bool enabled, bool allow_tristate, float height, ImFont* font, ImFont* summary_font)