Added ImGui Input Handler and ImGui Settings:

- Added ImGui Input Handler class that allows to customize handling of the keyboard and gamepad input.
- Added ImGui Settings to allow specify custom input handler implementation.
- Added editor support for ImGui Settings.
- Input handling in ImGui Widget is divided for querying the handler (more customizable) and actual input processing based on the handler’s response (fixed and simplified).
- Removed a need for checking console state in different input handling functions in ImGui Widget by suppressing keyboard focus support when console is opened.
This commit is contained in:
Sebastian 2018-07-10 17:40:57 +01:00
parent 6ddc7f2805
commit 5968c3ce84
12 changed files with 694 additions and 82 deletions

View File

@ -12,6 +12,12 @@ public class ImGui : ModuleRules
#endif #endif
{ {
#if WITH_FORWARDED_MODULE_RULES_CTOR
bool bBuildEditor = Target.bBuildEditor;
#else
bool bBuildEditor = (Target.Type == TargetRules.TargetType.Editor);
#endif
PublicIncludePaths.AddRange( PublicIncludePaths.AddRange(
new string[] { new string[] {
"ImGui/Public", "ImGui/Public",
@ -53,6 +59,17 @@ public class ImGui : ModuleRules
); );
if (bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Settings",
}
);
}
DynamicallyLoadedModuleNames.AddRange( DynamicallyLoadedModuleNames.AddRange(
new string[] new string[]
{ {

View File

@ -0,0 +1,106 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#if WITH_EDITOR
#include "ImGuiEditor.h"
#include "ImGuiSettings.h"
#include <ISettingsModule.h>
#define LOCTEXT_NAMESPACE "ImGuiEditor"
#define SETTINGS_CONTAINER TEXT("Project"), TEXT("Plugins"), TEXT("ImGui")
namespace
{
ISettingsModule* GetSettingsModule()
{
return FModuleManager::GetModulePtr<ISettingsModule>("Settings");
}
}
FImGuiEditor::FImGuiEditor()
{
Register();
// As a side effect of being part of the ImGui module, we need to support deferred registration (only executed if
// module is loaded manually at the very early stage).
if (!IsRegistrationCompleted())
{
CreateRegistrator();
}
}
FImGuiEditor::~FImGuiEditor()
{
Unregister();
}
void FImGuiEditor::Register()
{
// Only register after UImGuiSettings class is initialized (necessary to check in early loading stages).
if (!bSettingsRegistered && UImGuiSettings::StaticClass()->IsValidLowLevelFast())
{
if (ISettingsModule* SettingsModule = GetSettingsModule())
{
bSettingsRegistered = true;
SettingsModule->RegisterSettings(SETTINGS_CONTAINER,
LOCTEXT("ImGuiSettingsName", "ImGui"),
LOCTEXT("ImGuiSettingsDescription", "Configure the Unreal ImGui plugin."),
GetMutableDefault<UImGuiSettings>());
}
}
}
void FImGuiEditor::Unregister()
{
if (bSettingsRegistered)
{
bSettingsRegistered = false;
if (ISettingsModule* SettingsModule = GetSettingsModule())
{
SettingsModule->UnregisterSettings(SETTINGS_CONTAINER);
}
}
}
void FImGuiEditor::CreateRegistrator()
{
if (!RegistratorHandle.IsValid())
{
RegistratorHandle = FModuleManager::Get().OnModulesChanged().AddLambda([this](FName Name, EModuleChangeReason Reason)
{
if (Reason == EModuleChangeReason::ModuleLoaded)
{
Register();
}
if (IsRegistrationCompleted())
{
ReleaseRegistrator();
}
});
}
}
void FImGuiEditor::ReleaseRegistrator()
{
if (RegistratorHandle.IsValid())
{
FModuleManager::Get().OnModulesChanged().Remove(RegistratorHandle);
RegistratorHandle.Reset();
}
}
#undef SETTINGS_CONTAINER
#undef LOCTEXT_NAMESPACE
#endif // WITH_EDITOR

View File

@ -0,0 +1,30 @@
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#pragma once
#if WITH_EDITOR
// Registers module's settings in editor (due to a small size of this code we don't use a separate editor module).
class FImGuiEditor
{
public:
FImGuiEditor();
~FImGuiEditor();
private:
bool IsRegistrationCompleted() const { return bSettingsRegistered; }
void Register();
void Unregister();
void CreateRegistrator();
void ReleaseRegistrator();
FDelegateHandle RegistratorHandle;
bool bSettingsRegistered = false;
};
#endif // WITH_EDITOR

View File

@ -0,0 +1,42 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#include "ImGuiInputHandler.h"
#include "ImGuiContextProxy.h"
#include "ImGuiModuleManager.h"
#include <Engine/Console.h>
#include <Input/Events.h>
FImGuiInputResponse UImGuiInputHandler::OnKeyDown(const FKeyEvent& KeyEvent)
{
// Ignore console open events, so we don't block it from opening.
if (KeyEvent.GetKey() == EKeys::Tilde)
{
return FImGuiInputResponse{ false, false };
}
// Ignore escape event, if they are not meant for ImGui control.
if (KeyEvent.GetKey() == EKeys::Escape && !HasImGuiActiveItem())
{
return FImGuiInputResponse{ false, false };
}
return DefaultResponse();
}
bool UImGuiInputHandler::HasImGuiActiveItem() const
{
FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex);
return ContextProxy && ContextProxy->HasActiveItem();
}
void UImGuiInputHandler::Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex)
{
ModuleManager = InModuleManager;
GameViewport = InGameViewport;
ContextIndex = InContextIndex;
}

View File

@ -0,0 +1,55 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#include "ImGuiInputHandlerFactory.h"
#include "ImGuiInputHandler.h"
#include "ImGuiSettings.h"
DEFINE_LOG_CATEGORY_STATIC(LogImGuiInputHandlerFactory, Warning, All);
UImGuiInputHandler* FImGuiInputHandlerFactory::NewHandler(FImGuiModuleManager* ModuleManager, UGameViewportClient* GameViewport, int32 ContextIndex)
{
UClass* HandlerClass = nullptr;
if (UImGuiSettings* Settings = GetMutableDefault<UImGuiSettings>())
{
const auto& HandlerClassReference = Settings->GetImGuiInputHandlerClass();
if (HandlerClassReference.IsValid())
{
HandlerClass = HandlerClassReference.TryLoadClass<UImGuiInputHandler>();
if (!HandlerClass)
{
UE_LOG(LogImGuiInputHandlerFactory, Error, TEXT("Couldn't load ImGui Input Handler class '%s'."), *HandlerClassReference.ToString());
}
}
}
if (!HandlerClass)
{
HandlerClass = UImGuiInputHandler::StaticClass();
}
UImGuiInputHandler* Handler = NewObject<UImGuiInputHandler>(GameViewport, HandlerClass);
if (Handler)
{
Handler->Initialize(ModuleManager, GameViewport, ContextIndex);
Handler->AddToRoot();
}
else
{
UE_LOG(LogImGuiInputHandlerFactory, Error, TEXT("Failed attempt to create Input Handler: HandlerClass = %s."), *GetNameSafe(HandlerClass));
}
return Handler;
}
void FImGuiInputHandlerFactory::ReleaseHandler(UImGuiInputHandler* Handler)
{
if (Handler)
{
Handler->RemoveFromRoot();
}
}

View File

@ -0,0 +1,16 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#pragma once
class FImGuiModuleManager;
class UGameViewportClient;
class UImGuiInputHandler;
class FImGuiInputHandlerFactory
{
public:
static UImGuiInputHandler* NewHandler(FImGuiModuleManager* ModuleManager, UGameViewportClient* GameViewport, int32 ContextIndex);
static void ReleaseHandler(UImGuiInputHandler* Handler);
};

View File

@ -6,6 +6,10 @@
#include "Utilities/WorldContext.h" #include "Utilities/WorldContext.h"
#include "Utilities/WorldContextIndex.h" #include "Utilities/WorldContextIndex.h"
#if WITH_EDITOR
#include "Editor/ImGuiEditor.h"
#endif
#include <IPluginManager.h> #include <IPluginManager.h>
@ -32,6 +36,10 @@ struct EDelegateCategory
static FImGuiModuleManager* ModuleManager = nullptr; static FImGuiModuleManager* ModuleManager = nullptr;
#if WITH_EDITOR
static FImGuiEditor* ModuleEditor = nullptr;
#endif
#if WITH_EDITOR #if WITH_EDITOR
FImGuiDelegateHandle FImGuiModule::AddEditorImGuiDelegate(const FImGuiDelegate& Delegate) FImGuiDelegateHandle FImGuiModule::AddEditorImGuiDelegate(const FImGuiDelegate& Delegate)
{ {
@ -91,17 +99,28 @@ void FImGuiModule::RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle)
void FImGuiModule::StartupModule() void FImGuiModule::StartupModule()
{ {
checkf(!ModuleManager, TEXT("Instance of Module Manager already exists. Instance should be created only during module startup.")); // Create managers that implements module logic.
// Create module manager that implements modules logic. checkf(!ModuleManager, TEXT("Instance of the Module Manager already exists. Instance should be created only during module startup."));
ModuleManager = new FImGuiModuleManager(); ModuleManager = new FImGuiModuleManager();
#if WITH_EDITOR
checkf(!ModuleEditor, TEXT("Instance of the Module Editor already exists. Instance should be created only during module startup."));
ModuleEditor = new FImGuiEditor();
#endif
} }
void FImGuiModule::ShutdownModule() void FImGuiModule::ShutdownModule()
{ {
checkf(ModuleManager, TEXT("Null Module Manager. Manager instance should be deleted during module shutdown.")); // Before we shutdown we need to delete managers that will do all the necessary cleanup.
// Before we shutdown we need to delete manager that will do all necessary cleanup. #if WITH_EDITOR
checkf(ModuleEditor, TEXT("Null Module Editor. Module editor instance should be deleted during module shutdown."));
delete ModuleEditor;
ModuleEditor = nullptr;
#endif
checkf(ModuleManager, TEXT("Null Module Manager. Module manager instance should be deleted during module shutdown."));
delete ModuleManager; delete ModuleManager;
ModuleManager = nullptr; ModuleManager = nullptr;
} }

View File

@ -0,0 +1,51 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#include "ImGuiSettings.h"
UImGuiSettings::UImGuiSettings()
{
#if WITH_EDITOR
RegisterPropertyChangedDelegate();
#endif
}
UImGuiSettings::~UImGuiSettings()
{
#if WITH_EDITOR
UnregisterPropertyChangedDelegate();
#endif
}
#if WITH_EDITOR
void UImGuiSettings::RegisterPropertyChangedDelegate()
{
if (!FCoreUObjectDelegates::OnObjectPropertyChanged.IsBoundToObject(this))
{
FCoreUObjectDelegates::OnObjectPropertyChanged.AddUObject(this, &UImGuiSettings::OnPropertyChanged);
}
}
void UImGuiSettings::UnregisterPropertyChangedDelegate()
{
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
}
void UImGuiSettings::OnPropertyChanged(class UObject* ObjectBeingModified, struct FPropertyChangedEvent& PropertyChangedEvent)
{
if (ObjectBeingModified == this)
{
static const FName ImGuiInputHandlerPropertyName = GET_MEMBER_NAME_CHECKED(UImGuiSettings, ImGuiInputHandlerClass);
const FName UpdatedPropertyName = PropertyChangedEvent.MemberProperty ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None;
if (UpdatedPropertyName == ImGuiInputHandlerPropertyName)
{
OnImGuiInputHandlerClassChanged.Broadcast();
}
}
}
#endif // WITH_EDITOR

View File

@ -0,0 +1,56 @@
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "ImGuiInputHandler.h"
#include <Delegates/Delegate.h>
#include <UObject/Object.h>
// Select right soft class reference header to avoid warning (new header contains FSoftClassPath to FStringClassReference
// typedef, so we will use that as a common denominator).
#include <Runtime/Launch/Resources/Version.h>
#if (ENGINE_MAJOR_VERSION < 4 || (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION < 18))
#include <StringClassReference.h>
#else
#include <UObject/SoftObjectPath.h>
#endif
#include "ImGuiSettings.generated.h"
// Settings for ImGui module.
UCLASS(config=ImGui, defaultconfig)
class UImGuiSettings : public UObject
{
GENERATED_BODY()
public:
UImGuiSettings();
~UImGuiSettings();
// Path to custom implementation of ImGui Input Handler.
const FStringClassReference& GetImGuiInputHandlerClass() const { return ImGuiInputHandlerClass; }
// Delegate raised when ImGuiInputHandlerClass property has changed.
FSimpleMulticastDelegate OnImGuiInputHandlerClassChanged;
protected:
// Path to own implementation of ImGui Input Handler allowing to customize handling of keyboard and gamepad input.
// If not set then default handler is used.
UPROPERTY(EditAnywhere, config, Category = "Input", meta = (MetaClass = "ImGuiInputHandler"))
FStringClassReference ImGuiInputHandlerClass;
private:
#if WITH_EDITOR
void RegisterPropertyChangedDelegate();
void UnregisterPropertyChangedDelegate();
void OnPropertyChanged(class UObject* ObjectBeingModified, struct FPropertyChangedEvent& PropertyChangedEvent);
#endif // WITH_EDITOR
};

View File

@ -7,8 +7,11 @@
#include "ImGuiContextManager.h" #include "ImGuiContextManager.h"
#include "ImGuiContextProxy.h" #include "ImGuiContextProxy.h"
#include "ImGuiImplementation.h" #include "ImGuiImplementation.h"
#include "ImGuiInputHandler.h"
#include "ImGuiInputHandlerFactory.h"
#include "ImGuiInteroperability.h" #include "ImGuiInteroperability.h"
#include "ImGuiModuleManager.h" #include "ImGuiModuleManager.h"
#include "ImGuiSettings.h"
#include "TextureManager.h" #include "TextureManager.h"
#include "Utilities/Arrays.h" #include "Utilities/Arrays.h"
#include "Utilities/ScopeGuards.h" #include "Utilities/ScopeGuards.h"
@ -106,11 +109,19 @@ void SImGuiWidget::Construct(const FArguments& InArgs)
checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex);
ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw);
ContextProxy->SetInputState(&InputState); ContextProxy->SetInputState(&InputState);
// Create ImGui Input Handler.
CreateInputHandler();
RegisterInputHandlerChangedDelegate();
} }
END_SLATE_FUNCTION_BUILD_OPTIMIZATION END_SLATE_FUNCTION_BUILD_OPTIMIZATION
SImGuiWidget::~SImGuiWidget() SImGuiWidget::~SImGuiWidget()
{ {
// Release ImGui Input Handler.
UnregisterInputHandlerChangedDelegate();
ReleaseInputHandler();
// Remove binding between this widget and its context proxy. // Remove binding between this widget and its context proxy.
if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex))
{ {
@ -142,16 +153,23 @@ void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurren
UpdateInputEnabled(); UpdateInputEnabled();
} }
namespace
{
FReply ToSlateReply(const FImGuiInputResponse& HandlingResponse)
{
return HandlingResponse.HasConsumeRequest() ? FReply::Handled() : FReply::Unhandled();
}
}
FReply SImGuiWidget::OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) FReply SImGuiWidget::OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent)
{ {
if (IsConsoleOpened()) const FImGuiInputResponse Response = InputHandler->OnKeyChar(CharacterEvent);
if (Response.HasProcessingRequest())
{ {
return FReply::Unhandled(); InputState.AddCharacter(CharacterEvent.GetCharacter());
} }
InputState.AddCharacter(CharacterEvent.GetCharacter()); return ToSlateReply(Response);
return FReply::Handled();
} }
FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent)
@ -159,10 +177,14 @@ FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& Key
if (KeyEvent.GetKey().IsGamepadKey()) if (KeyEvent.GetKey().IsGamepadKey())
{ {
if (InputState.IsGamepadNavigationEnabled()) if (InputState.IsGamepadNavigationEnabled())
{
const FImGuiInputResponse Response = InputHandler->OnGamepadKeyDown(KeyEvent);
if (Response.HasProcessingRequest())
{ {
InputState.SetGamepadNavigationKey(KeyEvent, true); InputState.SetGamepadNavigationKey(KeyEvent, true);
}
return FReply::Handled(); return ToSlateReply(Response);
} }
else else
{ {
@ -171,17 +193,16 @@ FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& Key
} }
else else
{ {
if (IsConsoleOpened() || IgnoreKeyEvent(KeyEvent))
{
return FReply::Unhandled();
}
InputState.SetKeyDown(KeyEvent, true);
CopyModifierKeys(KeyEvent);
UpdateCanvasMapMode(KeyEvent); UpdateCanvasMapMode(KeyEvent);
return WithMouseLockRequests(FReply::Handled()); const FImGuiInputResponse Response = InputHandler->OnKeyDown(KeyEvent);
if (Response.HasProcessingRequest())
{
InputState.SetKeyDown(KeyEvent, true);
CopyModifierKeys(KeyEvent);
}
return WithMouseLockRequests(ToSlateReply(Response));
} }
} }
@ -191,9 +212,10 @@ FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEv
{ {
if (InputState.IsGamepadNavigationEnabled()) if (InputState.IsGamepadNavigationEnabled())
{ {
// Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state.
InputState.SetGamepadNavigationKey(KeyEvent, false); InputState.SetGamepadNavigationKey(KeyEvent, false);
return FReply::Handled(); return ToSlateReply(InputHandler->OnGamepadKeyUp(KeyEvent));
} }
else else
{ {
@ -202,25 +224,27 @@ FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEv
} }
else else
{ {
// Even if we don't send new keystrokes to ImGui, we still handle key up events, to make sure that we clear keys UpdateCanvasMapMode(KeyEvent);
// pressed before suppressing keyboard input.
// Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state.
InputState.SetKeyDown(KeyEvent, false); InputState.SetKeyDown(KeyEvent, false);
CopyModifierKeys(KeyEvent); CopyModifierKeys(KeyEvent);
UpdateCanvasMapMode(KeyEvent); return WithMouseLockRequests(ToSlateReply(InputHandler->OnKeyUp(KeyEvent)));
// If console is opened we notify key change but we also let event trough, so it can be handled by console.
return IsConsoleOpened() ? FReply::Unhandled() : WithMouseLockRequests(FReply::Handled());
} }
} }
FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent) FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent)
{ {
if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState.IsGamepadNavigationEnabled()) if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState.IsGamepadNavigationEnabled())
{
const FImGuiInputResponse Response = InputHandler->OnGamepadAxis(AnalogInputEvent);
if (Response.HasProcessingRequest())
{ {
InputState.SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); InputState.SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue());
}
return FReply::Handled(); return ToSlateReply(Response);
} }
else else
{ {
@ -275,24 +299,6 @@ FReply SImGuiWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEve
return FReply::Handled(); return FReply::Handled();
} }
FCursorReply SImGuiWidget::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const
{
EMouseCursor::Type MouseCursor = EMouseCursor::None;
if (MouseCursorOverride != EMouseCursor::None)
{
MouseCursor = MouseCursorOverride;
}
else if (CVars::DrawMouseCursor.GetValueOnGameThread() <= 0)
{
if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex))
{
MouseCursor = ContextProxy->GetMouseCursor();
}
}
return FCursorReply::Cursor(MouseCursor);
}
FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{ {
if (bCanvasMapMode) if (bCanvasMapMode)
@ -365,6 +371,66 @@ void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent)
UpdateInputMode(HasKeyboardFocus() && GameViewport->Viewport->IsForegroundWindow(), false); UpdateInputMode(HasKeyboardFocus() && GameViewport->Viewport->IsForegroundWindow(), false);
} }
FCursorReply SImGuiWidget::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const
{
EMouseCursor::Type MouseCursor = EMouseCursor::None;
if (MouseCursorOverride != EMouseCursor::None)
{
MouseCursor = MouseCursorOverride;
}
else if (CVars::DrawMouseCursor.GetValueOnGameThread() <= 0)
{
if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex))
{
MouseCursor = ContextProxy->GetMouseCursor();
}
}
return FCursorReply::Cursor(MouseCursor);
}
void SImGuiWidget::CreateInputHandler()
{
if (!InputHandler.IsValid())
{
InputHandler = FImGuiInputHandlerFactory::NewHandler(ModuleManager, GameViewport.Get(), ContextIndex);
}
}
void SImGuiWidget::ReleaseInputHandler()
{
if (InputHandler.IsValid())
{
FImGuiInputHandlerFactory::ReleaseHandler(InputHandler.Get());
InputHandler.Reset();
}
}
void SImGuiWidget::RecreateInputHandler()
{
ReleaseInputHandler();
CreateInputHandler();
}
void SImGuiWidget::RegisterInputHandlerChangedDelegate()
{
if (UImGuiSettings* Settings = GetMutableDefault<UImGuiSettings>())
{
if (!Settings->OnImGuiInputHandlerClassChanged.IsBoundToObject(this))
{
Settings->OnImGuiInputHandlerClassChanged.AddRaw(this, &SImGuiWidget::RecreateInputHandler);
}
}
}
void SImGuiWidget::UnregisterInputHandlerChangedDelegate()
{
if (UImGuiSettings* Settings = GetMutableDefault<UImGuiSettings>())
{
Settings->OnImGuiInputHandlerClassChanged.RemoveAll(this);
}
}
FReply SImGuiWidget::WithMouseLockRequests(FReply&& Reply) FReply SImGuiWidget::WithMouseLockRequests(FReply&& Reply)
{ {
const bool bNeedMouseLock = bCanvasDragging || bFrameDragging; const bool bNeedMouseLock = bCanvasDragging || bFrameDragging;
@ -404,27 +470,6 @@ bool SImGuiWidget::IsConsoleOpened() const
return GameViewport->ViewportConsole && GameViewport->ViewportConsole->ConsoleState != NAME_None; return GameViewport->ViewportConsole && GameViewport->ViewportConsole->ConsoleState != NAME_None;
} }
bool SImGuiWidget::IgnoreKeyEvent(const FKeyEvent& KeyEvent) const
{
// Ignore console open/close events.
if (KeyEvent.GetKey() == EKeys::Tilde)
{
return true;
}
// Ignore escape keys unless they are needed to cancel operations in ImGui.
if (KeyEvent.GetKey() == EKeys::Escape)
{
auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex);
if (!ContextProxy || !ContextProxy->HasActiveItem())
{
return true;
}
}
return false;
}
void SImGuiWidget::SetMouseCursorOverride(EMouseCursor::Type InMouseCursorOverride) void SImGuiWidget::SetMouseCursorOverride(EMouseCursor::Type InMouseCursorOverride)
{ {
if (MouseCursorOverride != InMouseCursorOverride) if (MouseCursorOverride != InMouseCursorOverride)

View File

@ -8,6 +8,7 @@
class FImGuiModuleManager; class FImGuiModuleManager;
class UImGuiInputHandler;
// Slate widget for rendering ImGui output and storing Slate inputs. // Slate widget for rendering ImGui output and storing Slate inputs.
class SImGuiWidget : public SLeafWidget class SImGuiWidget : public SLeafWidget
@ -45,7 +46,7 @@ public:
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
virtual bool SupportsKeyboardFocus() const override { return bInputEnabled; } virtual bool SupportsKeyboardFocus() const override { return bInputEnabled && !IsConsoleOpened(); }
virtual FReply OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) override; virtual FReply OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) override;
@ -63,8 +64,6 @@ public:
virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FCursorReply OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const override;
virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override; virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override;
@ -75,6 +74,8 @@ public:
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
virtual FCursorReply OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const override;
private: private:
enum class EInputMode : uint8 enum class EInputMode : uint8
@ -86,6 +87,13 @@ private:
Full Full
}; };
void CreateInputHandler();
void ReleaseInputHandler();
void RecreateInputHandler();
void RegisterInputHandlerChangedDelegate();
void UnregisterInputHandlerChangedDelegate();
// If needed, add to event reply a mouse lock or unlock request. // If needed, add to event reply a mouse lock or unlock request.
FORCEINLINE FReply WithMouseLockRequests(FReply&& Reply); FORCEINLINE FReply WithMouseLockRequests(FReply&& Reply);
@ -94,8 +102,6 @@ private:
bool IsConsoleOpened() const; bool IsConsoleOpened() const;
bool IgnoreKeyEvent(const FKeyEvent& KeyEvent) const;
void SetMouseCursorOverride(EMouseCursor::Type InMouseCursorOverride); void SetMouseCursorOverride(EMouseCursor::Type InMouseCursorOverride);
// Update visibility based on input enabled state. // Update visibility based on input enabled state.
@ -147,6 +153,7 @@ private:
FImGuiModuleManager* ModuleManager = nullptr; FImGuiModuleManager* ModuleManager = nullptr;
TWeakObjectPtr<UGameViewportClient> GameViewport; TWeakObjectPtr<UGameViewportClient> GameViewport;
TWeakObjectPtr<UImGuiInputHandler> InputHandler;
mutable TArray<FSlateVertex> VertexBuffer; mutable TArray<FSlateVertex> VertexBuffer;
mutable TArray<SlateIndex> IndexBuffer; mutable TArray<SlateIndex> IndexBuffer;

View File

@ -0,0 +1,168 @@
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#pragma once
#include <CoreMinimal.h>
#include <UObject/Object.h>
#include "ImGuiInputHandler.generated.h"
class FImGuiModuleManager;
class UGameViewportClient;
struct FAnalogInputEvent;
struct FCharacterEvent;
struct FKeyEvent;
/** 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/Input/ImGuiInputHandlerClass property to set custom implementation.
*/
UCLASS()
class IMGUI_API UImGuiInputHandler : public UObject
{
GENERATED_BODY()
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.
*/
virtual FImGuiInputResponse OnKeyChar(const struct FCharacterEvent& CharacterEvent) { return DefaultResponse(); }
/**
* 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.
*/
virtual FImGuiInputResponse 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.
*/
virtual FImGuiInputResponse OnKeyUp(const FKeyEvent& KeyEvent) { return DefaultResponse(); }
/**
* 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.
*/
virtual FImGuiInputResponse OnGamepadKeyDown(const FKeyEvent& GamepadKeyEvent) { return DefaultResponse(); }
/**
* 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.
*/
virtual FImGuiInputResponse OnGamepadKeyUp(const FKeyEvent& GamepadKeyEvent) { return DefaultResponse(); }
/**
* 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.
*/
virtual FImGuiInputResponse OnGamepadAxis(const FAnalogInputEvent& GamepadAxisEvent) { return DefaultResponse(); }
protected:
/** Checks whether corresponding ImGui context has an active item. */
bool HasImGuiActiveItem() const;
private:
void Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex);
FORCEINLINE FImGuiInputResponse DefaultResponse() { return FImGuiInputResponse{ true, true }; }
FImGuiModuleManager* ModuleManager = nullptr;
TWeakObjectPtr<UGameViewportClient> GameViewport;
int32 ContextIndex = -1;
friend class FImGuiInputHandlerFactory;
};