From c47d911f224c98c1121371221de0d1f8d7c025a2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Sep 2017 21:05:37 +0100 Subject: [PATCH] Fixed issues with passing input focus between viewport and ImGui Widget. --- Source/ImGui/Private/ImGuiModuleManager.cpp | 5 +- Source/ImGui/Private/SImGuiWidget.cpp | 89 ++++++++++++++++++--- Source/ImGui/Private/SImGuiWidget.h | 15 ++++ 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/Source/ImGui/Private/ImGuiModuleManager.cpp b/Source/ImGui/Private/ImGuiModuleManager.cpp index 65a1497..5995851 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.cpp +++ b/Source/ImGui/Private/ImGuiModuleManager.cpp @@ -157,9 +157,8 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) // Bind widget's input to context for this world. Proxy.SetInputState(&ViewportWidget->GetInputState()); - // High enough z-order guarantees that ImGui output is rendered on top of the game UI. - constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; - GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(ViewportWidget), IMGUI_WIDGET_Z_ORDER); + // We should always have one viewport per context index at a time (this will be validated by widget). + ViewportWidget->AttachToViewport(GameViewport); } void FImGuiModuleManager::AddWidgetToAllViewports() diff --git a/Source/ImGui/Private/SImGuiWidget.cpp b/Source/ImGui/Private/SImGuiWidget.cpp index e53c6b6..fddfe82 100644 --- a/Source/ImGui/Private/SImGuiWidget.cpp +++ b/Source/ImGui/Private/SImGuiWidget.cpp @@ -12,6 +12,10 @@ #include "Utilities/ScopeGuards.h" +// High enough z-order guarantees that ImGui output is rendered on top of the game UI. +constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; + + DEFINE_LOG_CATEGORY_STATIC(LogImGuiWidget, Warning, All); #define TEXT_INPUT_MODE(Val) ((Val) == EInputMode::MouseAndKeyboard ? TEXT("MouseAndKeyboard") : (Val) == EInputMode::MouseOnly ? TEXT("MouseOnly") : TEXT("None")) @@ -66,6 +70,23 @@ SImGuiWidget::~SImGuiWidget() ModuleManager->OnPostImGuiUpdate().RemoveAll(this); } +void SImGuiWidget::AttachToViewport(UGameViewportClient* InGameViewport, bool bResetInput) +{ + checkf(InGameViewport, TEXT("Null InGameViewport")); + checkf(!GameViewport.IsValid() || GameViewport.Get() == InGameViewport, + TEXT("Widget is attached to another game viewport and will be available for reuse only after this session ") + TEXT("ends. ContextIndex = %d, CurrentGameViewport = %s, InGameViewport = %s"), + ContextIndex, *GameViewport->GetName(), InGameViewport->GetName()); + + if (bResetInput) + { + ResetInputState(); + } + + GameViewport = InGameViewport; + GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(SharedThis(this)), IMGUI_WIDGET_Z_ORDER); +} + void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); @@ -209,6 +230,13 @@ void SImGuiWidget::CopyModifierKeys(const FPointerEvent& MouseEvent) } } +void SImGuiWidget::ResetInputState() +{ + bInputEnabled = false; + SetVisibilityFromInputEnabled(); + UpdateInputMode(false, false); +} + void SImGuiWidget::SetVisibilityFromInputEnabled() { // If we don't use input disable hit test to make this widget invisible for cursors hit detection. @@ -230,21 +258,33 @@ void SImGuiWidget::UpdateInputEnabled() SetVisibilityFromInputEnabled(); - // Setup input to show cursor and take focus when we use input or clear state and pass focus back to viewport - // when we don't. + // Setup input to show cursor and to pass keyboard/user focus between viewport and widget. Note that we should + // only pass focus if it is inside of the parent viewport, otherwise we would be stealing from other viewports + // or windows. auto& Slate = FSlateApplication::Get(); if (bInputEnabled) { - Slate.ResetToDefaultPointerInputSettings(); - Slate.SetKeyboardFocus(SharedThis(this)); + const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); + if (ViewportWidget->HasKeyboardFocus() || ViewportWidget->HasFocusedDescendants()) + { + // Remember where is user focus, so we will have an option to restore it. + PreviousUserFocusedWidget = Slate.GetUserFocusedWidget(Slate.GetUserIndexForKeyboard()); + + Slate.ResetToDefaultPointerInputSettings(); + Slate.SetKeyboardFocus(SharedThis(this)); + } } else { if (Slate.GetKeyboardFocusedWidget().Get() == this) { - Slate.SetUserFocusToGameViewport(Slate.GetUserIndexForKeyboard()); + Slate.ResetToDefaultPointerInputSettings(); + Slate.SetUserFocus(Slate.GetUserIndexForKeyboard(), + PreviousUserFocusedWidget.IsValid() ? PreviousUserFocusedWidget.Pin() : GameViewport->GetGameViewportWidget()); } + PreviousUserFocusedWidget.Reset(); + UpdateInputMode(false, false); } } @@ -338,6 +378,11 @@ FVector2D SImGuiWidget::ComputeDesiredSize(float) const // Controls tweaked for 2-columns layout. namespace TwoColumns { + static void GroupName(const char* Name) + { + ImGui::TextColored({ 0.5f, 0.5f, 0.5f, 1.f }, Name); ImGui::NextColumn(); ImGui::NextColumn(); + } + static void Value(const char* Label, int Value) { ImGui::Text("%s:", Label); ImGui::NextColumn(); @@ -362,12 +407,13 @@ void SImGuiWidget::OnDebugDraw() bool bDebug = CVars::DebugWidget.GetValueOnGameThread() > 0; if (bDebug) { - ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiSetCond_Once); + ImGui::SetNextWindowSize(ImVec2(380, 320), ImGuiSetCond_Once); if (ImGui::Begin("ImGui Widget Debug", &bDebug)) { ImGui::Columns(2, nullptr, false); TwoColumns::Value("Context Index", ContextIndex); + TwoColumns::Value("Game Viewport", *GameViewport->GetName()); ImGui::Separator(); @@ -376,10 +422,33 @@ void SImGuiWidget::OnDebugDraw() ImGui::Separator(); - TwoColumns::Value("Visibility", *GetVisibility().ToString()); - TwoColumns::Value("Is Hovered", IsHovered()); - TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered()); - TwoColumns::Value("Has Keyboard Input", HasKeyboardFocus()); + const float GroupIndent = 5.f; + + TwoColumns::GroupName("Widget"); + ImGui::Indent(GroupIndent); + { + TwoColumns::Value("Visibility", *GetVisibility().ToString()); + TwoColumns::Value("Is Hovered", IsHovered()); + TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered()); + TwoColumns::Value("Has Keyboard Input", HasKeyboardFocus()); + } + ImGui::Unindent(GroupIndent); + + ImGui::Separator(); + + TwoColumns::GroupName("Viewport Widget"); + ImGui::Indent(GroupIndent); + { + const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); + TwoColumns::Value("Is Hovered", ViewportWidget->IsHovered()); + TwoColumns::Value("Is Directly Hovered", ViewportWidget->IsDirectlyHovered()); + TwoColumns::Value("Has Mouse Capture", ViewportWidget->HasMouseCapture()); + TwoColumns::Value("Has Keyboard Input", ViewportWidget->HasKeyboardFocus()); + TwoColumns::Value("Has Focused Descendants", ViewportWidget->HasFocusedDescendants()); + auto Widget = PreviousUserFocusedWidget.Pin(); + TwoColumns::Value("Previous User Focused", Widget.IsValid() ? *Widget->GetTypeAsString() : TEXT("None")); + } + ImGui::Unindent(GroupIndent); ImGui::Columns(1); } diff --git a/Source/ImGui/Private/SImGuiWidget.h b/Source/ImGui/Private/SImGuiWidget.h index 5c78e8e..20f1ead 100644 --- a/Source/ImGui/Private/SImGuiWidget.h +++ b/Source/ImGui/Private/SImGuiWidget.h @@ -32,6 +32,16 @@ public: // Get input state associated with this widget. const FImGuiInputState& GetInputState() const { return InputState; } + // Get the game viewport to which this widget is attached. + const TWeakObjectPtr& GetGameViewport() const { return GameViewport; } + + // Attach this widget to a target game viewport. + // Widget can be attached to only one viewport at a time but can be reused after its last viewport becomes invalid + // at the end of a session. Widgets are weakly attached, so once destroyed they are automatically removed. + // @param InGameViewport - Target game viewport + // @param bResetInput - If true (default), input will be reset back to a default state + void AttachToViewport(UGameViewportClient* InGameViewport, bool bResetInput = true); + //---------------------------------------------------------------------------------------------------- // SWidget overrides //---------------------------------------------------------------------------------------------------- @@ -76,6 +86,8 @@ private: FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent); FORCEINLINE void CopyModifierKeys(const FPointerEvent& MouseEvent); + void ResetInputState(); + // Update visibility based on input enabled state. void SetVisibilityFromInputEnabled(); @@ -94,6 +106,7 @@ private: void OnDebugDraw(); FImGuiModuleManager* ModuleManager = nullptr; + TWeakObjectPtr GameViewport; mutable TArray VertexBuffer; mutable TArray IndexBuffer; @@ -104,4 +117,6 @@ private: bool bInputEnabled = false; FImGuiInputState InputState; + + TWeakPtr PreviousUserFocusedWidget; };