diff --git a/Source/ImGui/Private/ImGuiInputHandler.cpp b/Source/ImGui/Private/ImGuiInputHandler.cpp index 15adf42..a57cde1 100644 --- a/Source/ImGui/Private/ImGuiInputHandler.cpp +++ b/Source/ImGui/Private/ImGuiInputHandler.cpp @@ -5,6 +5,7 @@ #include "ImGuiInputHandler.h" #include "ImGuiContextProxy.h" +#include "ImGuiInputState.h" #include "ImGuiModuleManager.h" #include "ImGuiModuleSettings.h" @@ -20,44 +21,182 @@ DEFINE_LOG_CATEGORY(LogImGuiInputHandler); -static FImGuiInputResponse IgnoreResponse{ false, false }; - -FImGuiInputResponse UImGuiInputHandler::OnKeyDown(const FKeyEvent& KeyEvent) +namespace { - // Ignore console events, so we don't block it from opening. - if (IsConsoleEvent(KeyEvent)) + FReply ToReply(bool bConsume) { - return IgnoreResponse; + return bConsume ? FReply::Handled() : FReply::Unhandled(); } +} + +FReply UImGuiInputHandler::OnKeyChar(const struct FCharacterEvent& CharacterEvent) +{ + InputState->AddCharacter(CharacterEvent.GetCharacter()); + return ToReply(!ModuleManager->GetProperties().IsKeyboardInputShared()); +} + +FReply UImGuiInputHandler::OnKeyDown(const FKeyEvent& KeyEvent) +{ + if (KeyEvent.GetKey().IsGamepadKey()) + { + bool bConsume = false; + if (InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationKey(KeyEvent, true); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); + } + + return ToReply(bConsume); + } + else + { + // Ignore console events, so we don't block it from opening. + if (IsConsoleEvent(KeyEvent)) + { + return ToReply(false); + } #if WITH_EDITOR - // If there is no active ImGui control that would get precedence and this key event is bound to a stop play session - // command, then ignore that event and let the command execute. - if (!HasImGuiActiveItem() && IsStopPlaySessionEvent(KeyEvent)) - { - return IgnoreResponse; - } + // If there is no active ImGui control that would get precedence and this key event is bound to a stop play session + // command, then ignore that event and let the command execute. + if (!HasImGuiActiveItem() && IsStopPlaySessionEvent(KeyEvent)) + { + return ToReply(false); + } #endif // WITH_EDITOR - const FImGuiInputResponse Response = GetDefaultKeyboardResponse(); + const bool bConsume = !ModuleManager->GetProperties().IsKeyboardInputShared(); - // With shared input we can leave command bindings for DebugExec to handle, otherwise we need to do it here. - if (Response.HasConsumeRequest() && IsToggleInputEvent(KeyEvent)) + // With shared input we can leave command bindings for DebugExec to handle, otherwise we need to do it here. + if (bConsume && IsToggleInputEvent(KeyEvent)) + { + ModuleManager->GetProperties().ToggleInput(); + } + + InputState->SetKeyDown(KeyEvent, true); + CopyModifierKeys(KeyEvent); + + return ToReply(bConsume); + } +} + +FReply UImGuiInputHandler::OnKeyUp(const FKeyEvent& KeyEvent) +{ + if (KeyEvent.GetKey().IsGamepadKey()) { - ModuleManager->GetProperties().ToggleInput(); + bool bConsume = false; + if (InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationKey(KeyEvent, false); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); + } + + return ToReply(bConsume); + } + else + { + InputState->SetKeyDown(KeyEvent, false); + CopyModifierKeys(KeyEvent); + + return ToReply(!ModuleManager->GetProperties().IsKeyboardInputShared()); + } +} + +FReply UImGuiInputHandler::OnAnalogValueChanged(const FAnalogInputEvent& AnalogInputEvent) +{ + bool bConsume = false; + + if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); } - return Response; + return ToReply(bConsume); } -FImGuiInputResponse UImGuiInputHandler::GetDefaultKeyboardResponse() const +FReply UImGuiInputHandler::OnMouseButtonDown(const FPointerEvent& MouseEvent) { - return FImGuiInputResponse{ true, !ModuleManager->GetProperties().IsKeyboardInputShared() }; + InputState->SetMouseDown(MouseEvent, true); + return ToReply(true); } -FImGuiInputResponse UImGuiInputHandler::GetDefaultGamepadResponse() const +FReply UImGuiInputHandler::OnMouseButtonDoubleClick(const FPointerEvent& MouseEvent) { - return FImGuiInputResponse{ true, !ModuleManager->GetProperties().IsGamepadInputShared() }; + InputState->SetMouseDown(MouseEvent, true); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseButtonUp(const FPointerEvent& MouseEvent) +{ + InputState->SetMouseDown(MouseEvent, false); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseWheel(const FPointerEvent& MouseEvent) +{ + InputState->AddMouseWheelDelta(MouseEvent.GetWheelDelta()); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseMove(const FVector2D& MousePosition) +{ + InputState->SetMousePosition(MousePosition); + return ToReply(true); +} + +void UImGuiInputHandler::OnKeyboardInputEnabled() +{ + bKeyboardInputEnabled = true; +} + +void UImGuiInputHandler::OnKeyboardInputDisabled() +{ + if (bKeyboardInputEnabled) + { + bKeyboardInputEnabled = false; + InputState->ResetKeyboard(); + } +} + +void UImGuiInputHandler::OnGamepadInputEnabled() +{ + bGamepadInputEnabled = true; +} + +void UImGuiInputHandler::OnGamepadInputDisabled() +{ + if (bGamepadInputEnabled) + { + bGamepadInputEnabled = false; + InputState->ResetGamepadNavigation(); + } +} + +void UImGuiInputHandler::OnMouseInputEnabled() +{ + if (!bMouseInputEnabled) + { + bMouseInputEnabled = true; + UpdateInputStatePointer(); + } +} + +void UImGuiInputHandler::OnMouseInputDisabled() +{ + if (bMouseInputEnabled) + { + bMouseInputEnabled = false; + InputState->ResetMouse(); + UpdateInputStatePointer(); + } +} + +void UImGuiInputHandler::CopyModifierKeys(const FInputEvent& InputEvent) +{ + InputState->SetControlDown(InputEvent.IsControlDown()); + InputState->SetShiftDown(InputEvent.IsShiftDown()); + InputState->SetAltDown(InputEvent.IsAltDown()); } bool UImGuiInputHandler::IsConsoleEvent(const FKeyEvent& KeyEvent) const @@ -113,12 +252,47 @@ bool UImGuiInputHandler::HasImGuiActiveItem() const return ContextProxy && ContextProxy->HasActiveItem(); } +void UImGuiInputHandler::UpdateInputStatePointer() +{ + InputState->SetMousePointer(bMouseInputEnabled && ModuleManager->GetSettings().UseSoftwareCursor()); +} + +void UImGuiInputHandler::OnSoftwareCursorChanged(bool) +{ + UpdateInputStatePointer(); +} + +void UImGuiInputHandler::OnPostImGuiUpdate() +{ + InputState->ClearUpdateState(); + + // TODO Replace with delegates after adding property change events. + InputState->SetKeyboardNavigationEnabled(ModuleManager->GetProperties().IsKeyboardNavigationEnabled()); + InputState->SetGamepadNavigationEnabled(ModuleManager->GetProperties().IsGamepadNavigationEnabled()); + + const auto& PlatformApplication = FSlateApplication::Get().GetPlatformApplication(); + InputState->SetGamepad(PlatformApplication.IsValid() && PlatformApplication->IsGamepadAttached()); +} + void UImGuiInputHandler::Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex) { ModuleManager = InModuleManager; GameViewport = InGameViewport; ContextIndex = InContextIndex; + auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + checkf(ContextProxy, TEXT("Missing context during initialization of input handler: ContextIndex = %d"), ContextIndex); + InputState = &ContextProxy->GetInputState(); + + // Register to get post-update notifications, so we can clean frame updates. + ModuleManager->OnPostImGuiUpdate().AddUObject(this, &UImGuiInputHandler::OnPostImGuiUpdate); + + auto& Settings = ModuleManager->GetSettings(); + if (!Settings.OnUseSoftwareCursorChanged.IsBoundToObject(this)) + { + Settings.OnUseSoftwareCursorChanged.AddUObject(this, &UImGuiInputHandler::OnSoftwareCursorChanged); + } + #if WITH_EDITOR StopPlaySessionCommandInfo = FInputBindingManager::Get().FindCommandInContext("PlayWorld", "StopPlaySession"); if (!StopPlaySessionCommandInfo.IsValid()) @@ -128,3 +302,14 @@ void UImGuiInputHandler::Initialize(FImGuiModuleManager* InModuleManager, UGameV } #endif // WITH_EDITOR } + +void UImGuiInputHandler::BeginDestroy() +{ + Super::BeginDestroy(); + + if (ModuleManager) + { + ModuleManager->GetSettings().OnUseSoftwareCursorChanged.RemoveAll(this); + } +} + diff --git a/Source/ImGui/Private/Widgets/SImGuiWidget.cpp b/Source/ImGui/Private/Widgets/SImGuiWidget.cpp index adf6393..b04b3a2 100644 --- a/Source/ImGui/Private/Widgets/SImGuiWidget.cpp +++ b/Source/ImGui/Private/Widgets/SImGuiWidget.cpp @@ -7,10 +7,8 @@ #include "ImGuiContextManager.h" #include "ImGuiContextProxy.h" -#include "ImGuiImplementation.h" #include "ImGuiInputHandler.h" #include "ImGuiInputHandlerFactory.h" -#include "ImGuiInputState.h" #include "ImGuiInteroperability.h" #include "ImGuiModuleManager.h" #include "ImGuiModuleSettings.h" @@ -69,34 +67,28 @@ void SImGuiWidget::Construct(const FArguments& InArgs) GameViewport = InArgs._GameViewport; ContextIndex = InArgs._ContextIndex; - // Disable mouse cursor over this widget as we will use ImGui to draw it. - SetCursor(EMouseCursor::None); - - // Sync visibility with default input enabled state. - UpdateVisibility(); - - // Register to get post-update notifications, so we can clean frame updates. + // Register to get post-update notifications. ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); - // Bind this widget to its context proxy. + // Register debug delegate. auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); #if IMGUI_WIDGET_DEBUG ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); #endif // IMGUI_WIDGET_DEBUG - InputState = &ContextProxy->GetInputState(); // Register for settings change. RegisterImGuiSettingsDelegates(); + // Get initial settings. const auto& Settings = ModuleManager->GetSettings(); - - // Cache locally software cursor mode. - SetUseSoftwareCursor(Settings.UseSoftwareCursor()); - - // Create ImGui Input Handler. + SetHideMouseCursor(Settings.UseSoftwareCursor()); CreateInputHandler(Settings.GetImGuiInputHandlerClass()); + // Initialize state. + UpdateVisibility(); + UpdateMouseCursor(); + ChildSlot [ SAssignNew(CanvasControlWidget, SImGuiCanvasControl).OnTransformChanged(this, &SImGuiWidget::SetImGuiTransform) @@ -130,139 +122,74 @@ void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurren { Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); - // Note: Moving that update to console variable sink or callback might seem like a better alternative but input - // setup in this function is better handled here. - UpdateInputEnabled(); -} - -namespace -{ - FReply ToSlateReply(const FImGuiInputResponse& HandlingResponse) - { - return HandlingResponse.HasConsumeRequest() ? FReply::Handled() : FReply::Unhandled(); - } + UpdateInputState(); + HandleWindowFocusLost(); } FReply SImGuiWidget::OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) { - const FImGuiInputResponse Response = InputHandler->OnKeyChar(CharacterEvent); - if (Response.HasProcessingRequest()) - { - InputState->AddCharacter(CharacterEvent.GetCharacter()); - } - - return ToSlateReply(Response); + return InputHandler->OnKeyChar(CharacterEvent); } FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) { - if (KeyEvent.GetKey().IsGamepadKey()) - { - if (InputState->IsGamepadNavigationEnabled()) - { - const FImGuiInputResponse Response = InputHandler->OnGamepadKeyDown(KeyEvent); - if (Response.HasProcessingRequest()) - { - InputState->SetGamepadNavigationKey(KeyEvent, true); - } - - return ToSlateReply(Response); - } - else - { - return Super::OnKeyDown(MyGeometry, KeyEvent); - } - } - else - { - UpdateCanvasControlMode(KeyEvent); - - const FImGuiInputResponse Response = InputHandler->OnKeyDown(KeyEvent); - if (Response.HasProcessingRequest()) - { - InputState->SetKeyDown(KeyEvent, true); - CopyModifierKeys(KeyEvent); - } - - return ToSlateReply(Response); - } + UpdateCanvasControlMode(KeyEvent); + return InputHandler->OnKeyDown(KeyEvent); } FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) { - if (KeyEvent.GetKey().IsGamepadKey()) - { - if (InputState->IsGamepadNavigationEnabled()) - { - // Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state. - InputState->SetGamepadNavigationKey(KeyEvent, false); - - return ToSlateReply(InputHandler->OnGamepadKeyUp(KeyEvent)); - } - else - { - return Super::OnKeyUp(MyGeometry, KeyEvent); - } - } - else - { - UpdateCanvasControlMode(KeyEvent); - - // Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state. - InputState->SetKeyDown(KeyEvent, false); - CopyModifierKeys(KeyEvent); - - return ToSlateReply(InputHandler->OnKeyUp(KeyEvent)); - } + UpdateCanvasControlMode(KeyEvent); + return InputHandler->OnKeyUp(KeyEvent); } FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent) { - if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState->IsGamepadNavigationEnabled()) - { - const FImGuiInputResponse Response = InputHandler->OnGamepadAxis(AnalogInputEvent); - if (Response.HasProcessingRequest()) - { - InputState->SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); - } - - return ToSlateReply(Response); - } - else - { - return Super::OnAnalogValueChanged(MyGeometry, AnalogInputEvent); - } + return InputHandler->OnAnalogValueChanged(AnalogInputEvent); } FReply SImGuiWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - InputState->SetMouseDown(MouseEvent, true); - return FReply::Handled(); + return InputHandler->OnMouseButtonDown(MouseEvent).LockMouseToWidget(SharedThis(this)); } FReply SImGuiWidget::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - InputState->SetMouseDown(MouseEvent, true); - return FReply::Handled(); + return InputHandler->OnMouseButtonDoubleClick(MouseEvent).LockMouseToWidget(SharedThis(this)); +} + +namespace +{ + bool NeedMouseLock(const FPointerEvent& MouseEvent) + { +#if FROM_ENGINE_VERSION(4, 20) + return FSlateApplication::Get().GetPressedMouseButtons().Num() > 0; +#else + return MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::MiddleMouseButton) + || MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton); +#endif + } } FReply SImGuiWidget::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - InputState->SetMouseDown(MouseEvent, false); - return FReply::Handled(); + FReply Reply = InputHandler->OnMouseButtonUp(MouseEvent); + if (!NeedMouseLock(MouseEvent)) + { + Reply.ReleaseMouseLock(); + } + return Reply; } FReply SImGuiWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - InputState->AddMouseWheelDelta(MouseEvent.GetWheelDelta()); - return FReply::Handled(); + return InputHandler->OnMouseWheel(MouseEvent); } FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { const FSlateRenderTransform ImGuiToScreen = ImGuiTransform.Concatenate(MyGeometry.GetAccumulatedRenderTransform()); - InputState->SetMousePosition(ImGuiToScreen.Inverse().TransformPoint(MouseEvent.GetScreenSpacePosition())); - return FReply::Handled(); + return InputHandler->OnMouseMove(ImGuiToScreen.Inverse().TransformPoint(MouseEvent.GetScreenSpacePosition())); } FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) @@ -271,9 +198,9 @@ FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEv IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Received."), ContextIndex); - // If widget has a keyboard focus we always maintain mouse input. Technically, if mouse is outside of the widget - // area it won't generate events but we freeze its state until it either comes back or input is completely lost. - UpdateInputMode(true, IsDirectlyHovered()); + bForegroundWindow = GameViewport->Viewport->IsForegroundWindow(); + InputHandler->OnKeyboardInputEnabled(); + InputHandler->OnGamepadInputEnabled(); FSlateApplication::Get().ResetToDefaultPointerInputSettings(); return FReply::Handled(); @@ -285,7 +212,8 @@ void SImGuiWidget::OnFocusLost(const FFocusEvent& FocusEvent) IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Lost."), ContextIndex); - UpdateInputMode(false, IsDirectlyHovered()); + InputHandler->OnKeyboardInputDisabled(); + InputHandler->OnGamepadInputDisabled(); } void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) @@ -294,17 +222,7 @@ void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Enter."), ContextIndex); - // If mouse enters while input is active then we need to update mouse buttons because there is a chance that we - // missed some events. - if (InputMode != EInputMode::None) - { - for (const FKey& Button : { EKeys::LeftMouseButton, EKeys::MiddleMouseButton, EKeys::RightMouseButton, EKeys::ThumbMouseButton, EKeys::ThumbMouseButton2 }) - { - InputState->SetMouseDown(Button, MouseEvent.IsMouseButtonDown(Button)); - } - } - - UpdateInputMode(HasKeyboardFocus(), true); + InputHandler->OnMouseInputEnabled(); } void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) @@ -313,23 +231,7 @@ void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Leave."), ContextIndex); - // We don't get any events when application loses focus, but often this is followed by OnMouseLeave, so we can use - // this event to immediately disable keyboard input if application lost focus. - UpdateInputMode(HasKeyboardFocus() && GameViewport->Viewport->IsForegroundWindow(), false); -} - -FCursorReply SImGuiWidget::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const -{ - EMouseCursor::Type MouseCursor = EMouseCursor::None; - if (!bUseSoftwareCursor) - { - if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) - { - MouseCursor = ContextProxy->GetMouseCursor(); - } - } - - return FCursorReply::Cursor(MouseCursor); + InputHandler->OnMouseInputDisabled(); } void SImGuiWidget::CreateInputHandler(const FStringClassReference& HandlerClassReference) @@ -361,7 +263,7 @@ void SImGuiWidget::RegisterImGuiSettingsDelegates() } if (!Settings.OnUseSoftwareCursorChanged.IsBoundToObject(this)) { - Settings.OnUseSoftwareCursorChanged.AddRaw(this, &SImGuiWidget::SetUseSoftwareCursor); + Settings.OnUseSoftwareCursorChanged.AddRaw(this, &SImGuiWidget::SetHideMouseCursor); } } @@ -373,11 +275,13 @@ void SImGuiWidget::UnregisterImGuiSettingsDelegates() Settings.OnUseSoftwareCursorChanged.RemoveAll(this); } -void SImGuiWidget::CopyModifierKeys(const FInputEvent& InputEvent) +void SImGuiWidget::SetHideMouseCursor(bool bHide) { - InputState->SetControlDown(InputEvent.IsControlDown()); - InputState->SetShiftDown(InputEvent.IsShiftDown()); - InputState->SetAltDown(InputEvent.IsAltDown()); + if (bHideMouseCursor != bHide) + { + bHideMouseCursor = bHide; + UpdateMouseCursor(); + } } bool SImGuiWidget::IsConsoleOpened() const @@ -394,6 +298,19 @@ void SImGuiWidget::UpdateVisibility() ContextIndex, *GetVisibility().ToString()); } +void SImGuiWidget::UpdateMouseCursor() +{ + if (!bHideMouseCursor) + { + const FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + SetCursor(ContextProxy ? ContextProxy->GetMouseCursor() : EMouseCursor::Default); + } + else + { + SetCursor(EMouseCursor::None); + } +} + ULocalPlayer* SImGuiWidget::GetLocalPlayer() const { if (GameViewport.IsValid()) @@ -453,28 +370,39 @@ void SImGuiWidget::ReturnFocus() PreviousUserFocusedWidget.Reset(); } -void SImGuiWidget::UpdateInputEnabled() +void SImGuiWidget::UpdateInputState() { const bool bEnabled = ModuleManager && ModuleManager->GetProperties().IsInputEnabled(); if (bInputEnabled != bEnabled) { + IMGUI_WIDGET_LOG(Log, TEXT("ImGui Widget %d - Input Enabled changed to '%s'."), + ContextIndex, TEXT_BOOL(bEnabled)); + bInputEnabled = bEnabled; - IMGUI_WIDGET_LOG(Log, TEXT("ImGui Widget %d - Input Enabled changed to '%s'."), - ContextIndex, TEXT_BOOL(bInputEnabled)); - UpdateVisibility(); + UpdateMouseCursor(); - if (!bInputEnabled) + if (bInputEnabled) + { + // We won't get mouse enter, if viewport is already hovered. + if (GameViewport->GetGameViewportWidget()->IsHovered()) + { + InputHandler->OnMouseInputEnabled(); + } + + // Focus is handled later as it can depend on additional factors. + } + else { ReturnFocus(); - UpdateInputMode(false, false); } } - // Note: Some widgets, like console, can reset focus to viewport after we already grabbed it. If we detect that - // viewport has a focus while input is enabled we will take it. - if (bInputEnabled && !HasKeyboardFocus() && !IsConsoleOpened()) + // We should request a focus, if we are in the input mode and don't have one. But we should wait, if this is not + // a foreground window (application), if viewport doesn't have a focus or if console is opened. Note that this + // will keep this widget from releasing focus to viewport or other widgets as long as we are in the input mode. + if (bInputEnabled && GameViewport->Viewport->IsForegroundWindow() && !HasKeyboardFocus() && !IsConsoleOpened()) { const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); if (ViewportWidget->HasKeyboardFocus() || ViewportWidget->HasFocusedDescendants()) @@ -482,52 +410,33 @@ void SImGuiWidget::UpdateInputEnabled() TakeFocus(); } } - - // We don't get any events when application loses focus (we get OnMouseLeave but not always) but we fix it with - // this manual check. We still allow the above code to run, even if we need to suppress keyboard input right after - // that. - if (bInputEnabled && !GameViewport->Viewport->IsForegroundWindow() && InputMode == EInputMode::Full) - { - UpdateInputMode(false, IsDirectlyHovered()); - } - - if (bInputEnabled) - { - InputState->SetKeyboardNavigationEnabled(ModuleManager && ModuleManager->GetProperties().IsKeyboardNavigationEnabled()); - InputState->SetGamepadNavigationEnabled(ModuleManager && ModuleManager->GetProperties().IsGamepadNavigationEnabled()); - const auto& Application = FSlateApplication::Get().GetPlatformApplication(); - InputState->SetGamepad(Application.IsValid() && Application->IsGamepadAttached()); - } } -void SImGuiWidget::UpdateInputMode(bool bHasKeyboardFocus, bool bHasMousePointer) +void SImGuiWidget::HandleWindowFocusLost() { - const EInputMode NewInputMode = - bHasKeyboardFocus ? EInputMode::Full : - bHasMousePointer ? EInputMode::MousePointerOnly : - EInputMode::None; - - if (InputMode != NewInputMode) + // We can use window foreground status to notify about application losing or receiving focus. In some situations + // we get mouse leave or enter events, but they are only sent if mouse pointer is inside of the viewport. + if (bInputEnabled && HasKeyboardFocus()) { - IMGUI_WIDGET_LOG(Verbose, TEXT("ImGui Widget %d - Input Mode changed from '%s' to '%s'."), - ContextIndex, TEXT_INPUT_MODE(InputMode), TEXT_INPUT_MODE(NewInputMode)); - - // We need to reset input components if we are either fully shutting down or we are downgrading from full to - // mouse-only input mode. - if (NewInputMode == EInputMode::None) + if (bForegroundWindow != GameViewport->Viewport->IsForegroundWindow()) { - InputState->Reset(); - } - else if (InputMode == EInputMode::Full) - { - InputState->ResetKeyboard(); - InputState->ResetGamepadNavigation(); - } + bForegroundWindow = !bForegroundWindow; - InputMode = NewInputMode; + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Updating input after %s foreground window status."), + ContextIndex, bForegroundWindow ? TEXT("getting") : TEXT("losing")); + + if (bForegroundWindow) + { + InputHandler->OnKeyboardInputEnabled(); + InputHandler->OnGamepadInputEnabled(); + } + else + { + InputHandler->OnKeyboardInputDisabled(); + InputHandler->OnGamepadInputDisabled(); + } + } } - - InputState->SetMousePointer(bUseSoftwareCursor && bHasMousePointer); } void SImGuiWidget::UpdateCanvasControlMode(const FInputEvent& InputEvent) @@ -538,6 +447,7 @@ void SImGuiWidget::UpdateCanvasControlMode(const FInputEvent& InputEvent) void SImGuiWidget::OnPostImGuiUpdate() { ImGuiRenderTransform = ImGuiTransform; + UpdateMouseCursor(); } namespace @@ -755,6 +665,8 @@ namespace Styles void SImGuiWidget::OnDebugDraw() { + FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + if (CVars::DebugWidget.GetValueOnGameThread() > 0) { bool bDebug = true; @@ -766,7 +678,6 @@ void SImGuiWidget::OnDebugDraw() TwoColumns::CollapsingGroup("Context", [&]() { TwoColumns::Value("Context Index", ContextIndex); - FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); TwoColumns::Value("Context Name", ContextProxy ? *ContextProxy->GetName() : TEXT("< Null >")); TwoColumns::Value("Game Viewport", *GameViewport->GetName()); }); @@ -774,8 +685,6 @@ void SImGuiWidget::OnDebugDraw() TwoColumns::CollapsingGroup("Input Mode", [&]() { TwoColumns::Value("Input Enabled", bInputEnabled); - TwoColumns::Value("Input Mode", TEXT_INPUT_MODE(InputMode)); - TwoColumns::Value("Input Has Mouse Pointer", InputState->HasMousePointer()); }); TwoColumns::CollapsingGroup("Widget", [&]() @@ -807,8 +716,10 @@ void SImGuiWidget::OnDebugDraw() } } - if (CVars::DebugInput.GetValueOnGameThread() > 0) + if (ContextProxy && CVars::DebugInput.GetValueOnGameThread() > 0) { + FImGuiInputState& InputState = ContextProxy->GetInputState(); + bool bDebug = true; ImGui::SetNextWindowSize(ImVec2(460, 480), ImGuiSetCond_Once); if (ImGui::Begin("ImGui Input State", &bDebug)) @@ -832,7 +743,7 @@ void SImGuiWidget::OnDebugDraw() { const FKey& Key = Keys[Idx]; const uint32 KeyIndex = ImGuiInterops::GetKeyIndex(Key); - Styles::TextHighlight(InputState->GetKeys()[KeyIndex], [&]() + Styles::TextHighlight(InputState.GetKeys()[KeyIndex], [&]() { TwoColumns::Value(*Key.GetDisplayName().ToString(), KeyIndex); }); @@ -847,9 +758,9 @@ void SImGuiWidget::OnDebugDraw() Columns::CollapsingGroup("Modifier Keys", 4, [&]() { - Styles::TextHighlight(InputState->IsShiftDown(), [&]() { ImGui::Text("Shift"); }); ImGui::NextColumn(); - Styles::TextHighlight(InputState->IsControlDown(), [&]() { ImGui::Text("Control"); }); ImGui::NextColumn(); - Styles::TextHighlight(InputState->IsAltDown(), [&]() { ImGui::Text("Alt"); }); ImGui::NextColumn(); + Styles::TextHighlight(InputState.IsShiftDown(), [&]() { ImGui::Text("Shift"); }); ImGui::NextColumn(); + Styles::TextHighlight(InputState.IsControlDown(), [&]() { ImGui::Text("Control"); }); ImGui::NextColumn(); + Styles::TextHighlight(InputState.IsAltDown(), [&]() { ImGui::Text("Alt"); }); ImGui::NextColumn(); ImGui::NextColumn(); }); @@ -872,7 +783,7 @@ void SImGuiWidget::OnDebugDraw() { const FKey& Button = Buttons[Idx]; const uint32 MouseIndex = ImGuiInterops::GetMouseIndex(Button); - Styles::TextHighlight(InputState->GetMouseButtons()[MouseIndex], [&]() + Styles::TextHighlight(InputState.GetMouseButtons()[MouseIndex], [&]() { TwoColumns::Value(*Button.GetDisplayName().ToString(), MouseIndex); }); @@ -887,9 +798,9 @@ void SImGuiWidget::OnDebugDraw() Columns::CollapsingGroup("Mouse Axes", 4, [&]() { - TwoColumns::Value("Position X", InputState->GetMousePosition().X); - TwoColumns::Value("Position Y", InputState->GetMousePosition().Y); - TwoColumns::Value("Wheel Delta", InputState->GetMouseWheelDelta()); + TwoColumns::Value("Position X", InputState.GetMousePosition().X); + TwoColumns::Value("Position Y", InputState.GetMousePosition().Y); + TwoColumns::Value("Wheel Delta", InputState.GetMouseWheelDelta()); ImGui::NextColumn(); ImGui::NextColumn(); }); diff --git a/Source/ImGui/Private/Widgets/SImGuiWidget.h b/Source/ImGui/Private/Widgets/SImGuiWidget.h index 3a6dc24..1ecc243 100644 --- a/Source/ImGui/Private/Widgets/SImGuiWidget.h +++ b/Source/ImGui/Private/Widgets/SImGuiWidget.h @@ -11,7 +11,6 @@ // Hide ImGui Widget debug in non-developer mode. #define IMGUI_WIDGET_DEBUG IMGUI_MODULE_DEVELOPER -class FImGuiInputState; class FImGuiModuleManager; class SImGuiCanvasControl; class UImGuiInputHandler; @@ -71,42 +70,31 @@ public: virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; - virtual FCursorReply OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const override; - private: - enum class EInputMode : uint8 - { - None, - // Mouse pointer only without user focus - MousePointerOnly, - // Full input with user focus (mouse, keyboard and depending on navigation mode gamepad) - Full - }; - void CreateInputHandler(const FStringClassReference& HandlerClassReference); void ReleaseInputHandler(); - void SetUseSoftwareCursor(bool bUse) { bUseSoftwareCursor = bUse; } - void RegisterImGuiSettingsDelegates(); void UnregisterImGuiSettingsDelegates(); - FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent); + void SetHideMouseCursor(bool bHide); bool IsConsoleOpened() const; - // Update visibility based on input enabled state. + // Update visibility based on input state. void UpdateVisibility(); + // Update cursor based on input state. + void UpdateMouseCursor(); + ULocalPlayer* GetLocalPlayer() const; void TakeFocus(); void ReturnFocus(); - void UpdateInputEnabled(); - - // Determine new input mode based on hints. - void UpdateInputMode(bool bHasKeyboardFocus, bool bHasMousePointer); + // Update input state. + void UpdateInputState(); + void HandleWindowFocusLost(); void UpdateCanvasControlMode(const FInputEvent& InputEvent); @@ -134,13 +122,9 @@ private: int32 ContextIndex = 0; - FImGuiInputState* InputState; - - EInputMode InputMode = EInputMode::None; bool bInputEnabled = false; - - // Whether or not ImGui should draw its own cursor. - bool bUseSoftwareCursor = false; + bool bForegroundWindow = false; + bool bHideMouseCursor = true; TSharedPtr CanvasControlWidget; TWeakPtr PreviousUserFocusedWidget; diff --git a/Source/ImGui/Public/ImGuiInputHandler.h b/Source/ImGui/Public/ImGuiInputHandler.h index ec32b98..f490a58 100644 --- a/Source/ImGui/Public/ImGuiInputHandler.h +++ b/Source/ImGui/Public/ImGuiInputHandler.h @@ -21,77 +21,9 @@ class FUICommandInfo; #endif // WITH_EDITOR -/** Response used by ImGui Input Handler to communicate input handling requests. */ -struct IMGUI_API FImGuiInputResponse -{ - /** Create empty response with no requests. */ - FImGuiInputResponse() = default; - - /** - * Create response with custom request configuration. - * - * @param bInProcess - State of the processing request. - * @param bInConsume - State of the consume request. - */ - FImGuiInputResponse(bool bInProcess, bool bInConsume) - : bProcess(bInProcess) - , bConsume(bInConsume) - {} - - /** - * Check whether this response contains processing request. - * - * @returns True, if processing was requested and false otherwise. - */ - FORCEINLINE bool HasProcessingRequest() const { return bProcess; } - - /** - * Check whether this response contains consume request. - * - * @returns True, if consume was requested and false otherwise. - */ - FORCEINLINE bool HasConsumeRequest() const { return bConsume; } - - /** - * Set the processing request. - * - * @param bInProcess - True, to request input processing (implicit) and false otherwise. - * @returns Reference to this response (for chaining requests). - */ - FORCEINLINE FImGuiInputResponse& RequestProcessing(bool bInProcess = true) { bProcess = bInProcess; return *this; } - - /** - * Set the consume request. - * - * @param bInConsume - True, to request input consume (implicit) and false otherwise. - * @returns Reference to this response (for chaining requests). - */ - FORCEINLINE FImGuiInputResponse& RequestConsume(bool bInConsume = true) { bConsume = bInConsume; return *this; } - -private: - - bool bProcess = false; - - bool bConsume = false; -}; - /** - * Defines behaviour when handling input events. It allows to customize handling of the keyboard and gamepad input, - * primarily to support shortcuts in ImGui input mode. Since mouse is not really needed for this functionality and - * mouse pointer state and focus are closely connected to input mode, mouse events are left out of this interface. - * - * When receiving keyboard and gamepad events ImGui Widget calls input handler to query expected behaviour. By default, - * with a few exceptions (see @ OnKeyDown) all events are expected to be processed and consumed. Custom implementations - * may tweak that behaviour and/or inject custom code. - * - * Note that returned response is only treated as a hint. In current implementation all consume requests are respected - * but to protect from locking ImGui input states, key up events are always processed. Decision about blocking certain - * inputs can be taken during key down events and processing corresponding key up events should not make difference. - * - * Also note that input handler functions are only called when ImGui Widget is receiving input events, what can be for - * instance suppressed by opening console. - * - * See @ Project Settings/Plugins/ImGui/Extensions/ImGuiInputHandlerClass property to set custom implementation. + * Handles input and sends it to the input state, which is copied to the ImGui IO at the beginning of the frame. + * Implementation of the input handler can be changed in the ImGui project settings by changing ImGuiInputHandlerClass. */ UCLASS() class IMGUI_API UImGuiInputHandler : public UObject @@ -101,77 +33,85 @@ class IMGUI_API UImGuiInputHandler : public UObject public: /** - * Called when handling character events. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to process - * and consume this event. + * Called to handle character events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnKeyChar(const struct FCharacterEvent& CharacterEvent) { return GetDefaultKeyboardResponse(); } + virtual FReply OnKeyChar(const struct FCharacterEvent& CharacterEvent); /** - * Called when handling keyboard key down events. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to process - * and consume most of the key, but unlike other cases it requests to ignore certain events, like those that are - * needed to open console or close PIE session in editor. + * Called to handle key down events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnKeyDown(const FKeyEvent& KeyEvent); + virtual FReply OnKeyDown(const FKeyEvent& KeyEvent); /** - * Called when handling keyboard key up events. - * - * Note that regardless of returned response, key up events are always processed by ImGui Widget. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to consume - * this event. + * Called to handle key up events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnKeyUp(const FKeyEvent& KeyEvent) { return GetDefaultKeyboardResponse(); } + virtual FReply OnKeyUp(const FKeyEvent& KeyEvent); /** - * Called when handling gamepad key down events. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to process - * and consume this event. + * Called to handle analog value change events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnGamepadKeyDown(const FKeyEvent& GamepadKeyEvent) { return GetDefaultGamepadResponse(); } + virtual FReply OnAnalogValueChanged(const FAnalogInputEvent& AnalogInputEvent); /** - * Called when handling gamepad key up events. - * - * Note that regardless of returned response, key up events are always processed by ImGui Widget. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to consume - * this event. + * Called to handle mouse button down events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnGamepadKeyUp(const FKeyEvent& GamepadKeyEvent) { return GetDefaultGamepadResponse(); } + virtual FReply OnMouseButtonDown(const FPointerEvent& MouseEvent); /** - * Called when handling gamepad analog events. - * - * @returns Response with rules how input should be handled. Default implementation contains requests to process - * and consume this event. + * Called to handle mouse button double-click events. + * @returns Response whether the event was handled */ - virtual FImGuiInputResponse OnGamepadAxis(const FAnalogInputEvent& GamepadAxisEvent) { return GetDefaultGamepadResponse(); } + virtual FReply OnMouseButtonDoubleClick(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse button up events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseButtonUp(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse wheel events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseWheel(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse move events. + * @param Mouse position (in ImGui space) + * @returns Response whether the event was handled + */ + virtual FReply OnMouseMove(const FVector2D& MousePosition); + + /** Called to handle activation of the keyboard input. */ + virtual void OnKeyboardInputEnabled(); + + /** Called to handle deactivation of the keyboard input. */ + virtual void OnKeyboardInputDisabled(); + + /** Called to handle activation of the gamepad input. */ + virtual void OnGamepadInputEnabled(); + + /** Called to handle deactivation of the gamepad input. */ + virtual void OnGamepadInputDisabled(); + + /** Called to handle activation of the mouse input. */ + virtual void OnMouseInputEnabled(); + + /** Called to handle deactivation of the mouse input. */ + virtual void OnMouseInputDisabled(); protected: - /** - * Get default keyboard response, with consume request based on IsKeyboardInputShared property. - * - * @returns Default response for keyboard inputs. - */ - FImGuiInputResponse GetDefaultKeyboardResponse() const; - - /** - * Get default gamepad response, with consume request based on IsGamepadInputShared property. - * - * @returns Default response for gamepad inputs. - */ - FImGuiInputResponse GetDefaultGamepadResponse() const; + /** Copy state of modifier keys to input state. */ + void CopyModifierKeys(const FInputEvent& InputEvent); /** * Checks whether this is a key event that can open console. - * * @param KeyEvent - Key event to test. * @returns True, if this key event can open console. */ @@ -180,7 +120,6 @@ protected: #if WITH_EDITOR /** * Checks whether this is a key event that can stop PIE session. - * * @param KeyEvent - Key event to test. * @returns True, if this key event can stop PIE session. */ @@ -189,7 +128,6 @@ protected: /** * Checks whether this key event can toggle ImGui input (as defined in settings). - * * @param KeyEvent - Key event to test. * @returns True, if this key is bound to 'ImGui.ToggleInput' command that switches ImGui input mode. */ @@ -197,15 +135,28 @@ protected: /** * Checks whether corresponding ImGui context has an active item (holding cursor focus). - * * @returns True, if corresponding context has an active item. */ bool HasImGuiActiveItem() const; private: + void UpdateInputStatePointer(); + + void OnSoftwareCursorChanged(bool); + + void OnPostImGuiUpdate(); + void Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex); + virtual void BeginDestroy() override; + + class FImGuiInputState* InputState = nullptr; + + bool bMouseInputEnabled = false; + bool bKeyboardInputEnabled = false; + bool bGamepadInputEnabled = false; + FImGuiModuleManager* ModuleManager = nullptr; TWeakObjectPtr GameViewport;