Fixed issues with passing input focus between viewport and ImGui Widget.

This commit is contained in:
Sebastian 2017-09-10 21:05:37 +01:00
parent f696393089
commit c47d911f22
3 changed files with 96 additions and 13 deletions

View File

@ -157,9 +157,8 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport)
// Bind widget's input to context for this world. // Bind widget's input to context for this world.
Proxy.SetInputState(&ViewportWidget->GetInputState()); Proxy.SetInputState(&ViewportWidget->GetInputState());
// High enough z-order guarantees that ImGui output is rendered on top of the game UI. // We should always have one viewport per context index at a time (this will be validated by widget).
constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; ViewportWidget->AttachToViewport(GameViewport);
GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(ViewportWidget), IMGUI_WIDGET_Z_ORDER);
} }
void FImGuiModuleManager::AddWidgetToAllViewports() void FImGuiModuleManager::AddWidgetToAllViewports()

View File

@ -12,6 +12,10 @@
#include "Utilities/ScopeGuards.h" #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_LOG_CATEGORY_STATIC(LogImGuiWidget, Warning, All);
#define TEXT_INPUT_MODE(Val) ((Val) == EInputMode::MouseAndKeyboard ? TEXT("MouseAndKeyboard") : (Val) == EInputMode::MouseOnly ? TEXT("MouseOnly") : TEXT("None")) #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); 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) void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{ {
Super::Tick(AllottedGeometry, InCurrentTime, 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() void SImGuiWidget::SetVisibilityFromInputEnabled()
{ {
// If we don't use input disable hit test to make this widget invisible for cursors hit detection. // 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(); SetVisibilityFromInputEnabled();
// Setup input to show cursor and take focus when we use input or clear state and pass focus back to viewport // Setup input to show cursor and to pass keyboard/user focus between viewport and widget. Note that we should
// when we don't. // 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(); auto& Slate = FSlateApplication::Get();
if (bInputEnabled) if (bInputEnabled)
{ {
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.ResetToDefaultPointerInputSettings();
Slate.SetKeyboardFocus(SharedThis(this)); Slate.SetKeyboardFocus(SharedThis(this));
} }
}
else else
{ {
if (Slate.GetKeyboardFocusedWidget().Get() == this) 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); UpdateInputMode(false, false);
} }
} }
@ -338,6 +378,11 @@ FVector2D SImGuiWidget::ComputeDesiredSize(float) const
// Controls tweaked for 2-columns layout. // Controls tweaked for 2-columns layout.
namespace TwoColumns 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) static void Value(const char* Label, int Value)
{ {
ImGui::Text("%s:", Label); ImGui::NextColumn(); ImGui::Text("%s:", Label); ImGui::NextColumn();
@ -362,12 +407,13 @@ void SImGuiWidget::OnDebugDraw()
bool bDebug = CVars::DebugWidget.GetValueOnGameThread() > 0; bool bDebug = CVars::DebugWidget.GetValueOnGameThread() > 0;
if (bDebug) if (bDebug)
{ {
ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiSetCond_Once); ImGui::SetNextWindowSize(ImVec2(380, 320), ImGuiSetCond_Once);
if (ImGui::Begin("ImGui Widget Debug", &bDebug)) if (ImGui::Begin("ImGui Widget Debug", &bDebug))
{ {
ImGui::Columns(2, nullptr, false); ImGui::Columns(2, nullptr, false);
TwoColumns::Value("Context Index", ContextIndex); TwoColumns::Value("Context Index", ContextIndex);
TwoColumns::Value("Game Viewport", *GameViewport->GetName());
ImGui::Separator(); ImGui::Separator();
@ -376,10 +422,33 @@ void SImGuiWidget::OnDebugDraw()
ImGui::Separator(); ImGui::Separator();
const float GroupIndent = 5.f;
TwoColumns::GroupName("Widget");
ImGui::Indent(GroupIndent);
{
TwoColumns::Value("Visibility", *GetVisibility().ToString()); TwoColumns::Value("Visibility", *GetVisibility().ToString());
TwoColumns::Value("Is Hovered", IsHovered()); TwoColumns::Value("Is Hovered", IsHovered());
TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered()); TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered());
TwoColumns::Value("Has Keyboard Input", HasKeyboardFocus()); 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); ImGui::Columns(1);
} }

View File

@ -32,6 +32,16 @@ public:
// Get input state associated with this widget. // Get input state associated with this widget.
const FImGuiInputState& GetInputState() const { return InputState; } const FImGuiInputState& GetInputState() const { return InputState; }
// Get the game viewport to which this widget is attached.
const TWeakObjectPtr<UGameViewportClient>& 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 // SWidget overrides
//---------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------
@ -76,6 +86,8 @@ private:
FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent); FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent);
FORCEINLINE void CopyModifierKeys(const FPointerEvent& MouseEvent); FORCEINLINE void CopyModifierKeys(const FPointerEvent& MouseEvent);
void ResetInputState();
// Update visibility based on input enabled state. // Update visibility based on input enabled state.
void SetVisibilityFromInputEnabled(); void SetVisibilityFromInputEnabled();
@ -94,6 +106,7 @@ private:
void OnDebugDraw(); void OnDebugDraw();
FImGuiModuleManager* ModuleManager = nullptr; FImGuiModuleManager* ModuleManager = nullptr;
TWeakObjectPtr<UGameViewportClient> GameViewport;
mutable TArray<FSlateVertex> VertexBuffer; mutable TArray<FSlateVertex> VertexBuffer;
mutable TArray<SlateIndex> IndexBuffer; mutable TArray<SlateIndex> IndexBuffer;
@ -104,4 +117,6 @@ private:
bool bInputEnabled = false; bool bInputEnabled = false;
FImGuiInputState InputState; FImGuiInputState InputState;
TWeakPtr<SWidget> PreviousUserFocusedWidget;
}; };