Moved the whole input handling from SImGuiWidget to UImGuiInputHandler:

- Moved responsibility for updating input state from the widget to the input handler.
- Changed the widget to fully delegate input events to the input handler and only manage its own input state.
- Changed the input handler interface to allow handling of all the necessary input events.
- Changed the input handler interface to use FReply as a response type and removed obsolete FImGuiInputResponse.
This commit is contained in:
Sebastian 2019-06-30 18:25:58 +01:00
parent 852a501022
commit 979903722a
4 changed files with 409 additions and 378 deletions

View File

@ -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);
}
}

View File

@ -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();
});

View File

@ -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<SImGuiCanvasControl> CanvasControlWidget;
TWeakPtr<SWidget> PreviousUserFocusedWidget;

View File

@ -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<UGameViewportClient> GameViewport;