diff --git a/Source/ImGui/ImGui.Build.cs b/Source/ImGui/ImGui.Build.cs index fe56412..8651ff9 100644 --- a/Source/ImGui/ImGui.Build.cs +++ b/Source/ImGui/ImGui.Build.cs @@ -11,7 +11,13 @@ public class ImGui : ModuleRules public ImGui(TargetInfo Target) #endif { - + +#if WITH_FORWARDED_MODULE_RULES_CTOR + bool bBuildEditor = Target.bBuildEditor; +#else + bool bBuildEditor = (Target.Type == TargetRules.TargetType.Editor); +#endif + PublicIncludePaths.AddRange( new string[] { "ImGui/Public", @@ -19,8 +25,8 @@ public class ImGui : ModuleRules // ... add public include paths required here ... } ); - - + + PrivateIncludePaths.AddRange( new string[] { "ImGui/Private", @@ -28,8 +34,8 @@ public class ImGui : ModuleRules // ... add other private include paths required here ... } ); - - + + PublicDependencyModuleNames.AddRange( new string[] { @@ -38,8 +44,8 @@ public class ImGui : ModuleRules // ... add other public dependencies that you statically link with here ... } ); - - + + PrivateDependencyModuleNames.AddRange( new string[] { @@ -51,8 +57,19 @@ public class ImGui : ModuleRules // ... add private dependencies that you statically link with here ... } ); - - + + + if (bBuildEditor) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Settings", + } + ); + } + + DynamicallyLoadedModuleNames.AddRange( new string[] { diff --git a/Source/ImGui/Private/Editor/ImGuiEditor.cpp b/Source/ImGui/Private/Editor/ImGuiEditor.cpp new file mode 100644 index 0000000..781ea74 --- /dev/null +++ b/Source/ImGui/Private/Editor/ImGuiEditor.cpp @@ -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 + + +#define LOCTEXT_NAMESPACE "ImGuiEditor" + +#define SETTINGS_CONTAINER TEXT("Project"), TEXT("Plugins"), TEXT("ImGui") + + +namespace +{ + ISettingsModule* GetSettingsModule() + { + return FModuleManager::GetModulePtr("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()); + } + } +} + +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 diff --git a/Source/ImGui/Private/Editor/ImGuiEditor.h b/Source/ImGui/Private/Editor/ImGuiEditor.h new file mode 100644 index 0000000..61a2aee --- /dev/null +++ b/Source/ImGui/Private/Editor/ImGuiEditor.h @@ -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 diff --git a/Source/ImGui/Private/ImGuiInputHandler.cpp b/Source/ImGui/Private/ImGuiInputHandler.cpp new file mode 100644 index 0000000..8288820 --- /dev/null +++ b/Source/ImGui/Private/ImGuiInputHandler.cpp @@ -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 +#include + + +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; +} diff --git a/Source/ImGui/Private/ImGuiInputHandlerFactory.cpp b/Source/ImGui/Private/ImGuiInputHandlerFactory.cpp new file mode 100644 index 0000000..ecd5061 --- /dev/null +++ b/Source/ImGui/Private/ImGuiInputHandlerFactory.cpp @@ -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()) + { + const auto& HandlerClassReference = Settings->GetImGuiInputHandlerClass(); + if (HandlerClassReference.IsValid()) + { + HandlerClass = HandlerClassReference.TryLoadClass(); + + 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(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(); + } +} diff --git a/Source/ImGui/Private/ImGuiInputHandlerFactory.h b/Source/ImGui/Private/ImGuiInputHandlerFactory.h new file mode 100644 index 0000000..3186317 --- /dev/null +++ b/Source/ImGui/Private/ImGuiInputHandlerFactory.h @@ -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); +}; diff --git a/Source/ImGui/Private/ImGuiModule.cpp b/Source/ImGui/Private/ImGuiModule.cpp index a8478f3..f07301e 100644 --- a/Source/ImGui/Private/ImGuiModule.cpp +++ b/Source/ImGui/Private/ImGuiModule.cpp @@ -6,6 +6,10 @@ #include "Utilities/WorldContext.h" #include "Utilities/WorldContextIndex.h" +#if WITH_EDITOR +#include "Editor/ImGuiEditor.h" +#endif + #include @@ -32,6 +36,10 @@ struct EDelegateCategory static FImGuiModuleManager* ModuleManager = nullptr; +#if WITH_EDITOR +static FImGuiEditor* ModuleEditor = nullptr; +#endif + #if WITH_EDITOR FImGuiDelegateHandle FImGuiModule::AddEditorImGuiDelegate(const FImGuiDelegate& Delegate) { @@ -91,17 +99,28 @@ void FImGuiModule::RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle) 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(); + +#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() { - 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; ModuleManager = nullptr; } diff --git a/Source/ImGui/Private/ImGuiSettings.cpp b/Source/ImGui/Private/ImGuiSettings.cpp new file mode 100644 index 0000000..b64e338 --- /dev/null +++ b/Source/ImGui/Private/ImGuiSettings.cpp @@ -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 diff --git a/Source/ImGui/Private/ImGuiSettings.h b/Source/ImGui/Private/ImGuiSettings.h new file mode 100644 index 0000000..315e7fe --- /dev/null +++ b/Source/ImGui/Private/ImGuiSettings.h @@ -0,0 +1,56 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ImGuiInputHandler.h" + +#include +#include + +// 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 +#if (ENGINE_MAJOR_VERSION < 4 || (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION < 18)) +#include +#else +#include +#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 +}; diff --git a/Source/ImGui/Private/SImGuiWidget.cpp b/Source/ImGui/Private/SImGuiWidget.cpp index dbd0ed1..e2e2781 100644 --- a/Source/ImGui/Private/SImGuiWidget.cpp +++ b/Source/ImGui/Private/SImGuiWidget.cpp @@ -7,8 +7,11 @@ #include "ImGuiContextManager.h" #include "ImGuiContextProxy.h" #include "ImGuiImplementation.h" +#include "ImGuiInputHandler.h" +#include "ImGuiInputHandlerFactory.h" #include "ImGuiInteroperability.h" #include "ImGuiModuleManager.h" +#include "ImGuiSettings.h" #include "TextureManager.h" #include "Utilities/Arrays.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); ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); ContextProxy->SetInputState(&InputState); + + // Create ImGui Input Handler. + CreateInputHandler(); + RegisterInputHandlerChangedDelegate(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION SImGuiWidget::~SImGuiWidget() { + // Release ImGui Input Handler. + UnregisterInputHandlerChangedDelegate(); + ReleaseInputHandler(); + // Remove binding between this widget and its context proxy. if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { @@ -142,16 +153,23 @@ void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurren UpdateInputEnabled(); } +namespace +{ + FReply ToSlateReply(const FImGuiInputResponse& HandlingResponse) + { + return HandlingResponse.HasConsumeRequest() ? FReply::Handled() : FReply::Unhandled(); + } +} + 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 FReply::Handled(); + return ToSlateReply(Response); } FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) @@ -160,9 +178,13 @@ FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& Key { if (InputState.IsGamepadNavigationEnabled()) { - InputState.SetGamepadNavigationKey(KeyEvent, true); + const FImGuiInputResponse Response = InputHandler->OnGamepadKeyDown(KeyEvent); + if (Response.HasProcessingRequest()) + { + InputState.SetGamepadNavigationKey(KeyEvent, true); + } - return FReply::Handled(); + return ToSlateReply(Response); } else { @@ -171,17 +193,16 @@ FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& Key } else { - if (IsConsoleOpened() || IgnoreKeyEvent(KeyEvent)) - { - return FReply::Unhandled(); - } - - InputState.SetKeyDown(KeyEvent, true); - CopyModifierKeys(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()) { + // Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state. InputState.SetGamepadNavigationKey(KeyEvent, false); - return FReply::Handled(); + return ToSlateReply(InputHandler->OnGamepadKeyUp(KeyEvent)); } else { @@ -202,15 +224,13 @@ FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEv } else { - // Even if we don't send new keystrokes to ImGui, we still handle key up events, to make sure that we clear keys - // pressed before suppressing keyboard input. + UpdateCanvasMapMode(KeyEvent); + + // Always handle key up events to protect from leaving accidental keys not cleared in ImGui input state. InputState.SetKeyDown(KeyEvent, false); CopyModifierKeys(KeyEvent); - UpdateCanvasMapMode(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()); + return WithMouseLockRequests(ToSlateReply(InputHandler->OnKeyUp(KeyEvent))); } } @@ -218,9 +238,13 @@ FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAn { if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState.IsGamepadNavigationEnabled()) { - InputState.SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); + const FImGuiInputResponse Response = InputHandler->OnGamepadAxis(AnalogInputEvent); + if (Response.HasProcessingRequest()) + { + InputState.SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); + } - return FReply::Handled(); + return ToSlateReply(Response); } else { @@ -275,24 +299,6 @@ FReply SImGuiWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEve 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) { if (bCanvasMapMode) @@ -365,6 +371,66 @@ void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) 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()) + { + if (!Settings->OnImGuiInputHandlerClassChanged.IsBoundToObject(this)) + { + Settings->OnImGuiInputHandlerClassChanged.AddRaw(this, &SImGuiWidget::RecreateInputHandler); + } + } +} + +void SImGuiWidget::UnregisterInputHandlerChangedDelegate() +{ + if (UImGuiSettings* Settings = GetMutableDefault()) + { + Settings->OnImGuiInputHandlerClassChanged.RemoveAll(this); + } +} + FReply SImGuiWidget::WithMouseLockRequests(FReply&& Reply) { const bool bNeedMouseLock = bCanvasDragging || bFrameDragging; @@ -404,27 +470,6 @@ bool SImGuiWidget::IsConsoleOpened() const 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) { if (MouseCursorOverride != InMouseCursorOverride) diff --git a/Source/ImGui/Private/SImGuiWidget.h b/Source/ImGui/Private/SImGuiWidget.h index 7a6e502..957fe69 100644 --- a/Source/ImGui/Private/SImGuiWidget.h +++ b/Source/ImGui/Private/SImGuiWidget.h @@ -8,6 +8,7 @@ class FImGuiModuleManager; +class UImGuiInputHandler; // Slate widget for rendering ImGui output and storing Slate inputs. class SImGuiWidget : public SLeafWidget @@ -45,7 +46,7 @@ public: 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; @@ -63,8 +64,6 @@ public: 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 OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override; @@ -75,6 +74,8 @@ public: virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; + virtual FCursorReply OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const override; + private: enum class EInputMode : uint8 @@ -86,6 +87,13 @@ private: Full }; + void CreateInputHandler(); + void ReleaseInputHandler(); + void RecreateInputHandler(); + + void RegisterInputHandlerChangedDelegate(); + void UnregisterInputHandlerChangedDelegate(); + // If needed, add to event reply a mouse lock or unlock request. FORCEINLINE FReply WithMouseLockRequests(FReply&& Reply); @@ -94,8 +102,6 @@ private: bool IsConsoleOpened() const; - bool IgnoreKeyEvent(const FKeyEvent& KeyEvent) const; - void SetMouseCursorOverride(EMouseCursor::Type InMouseCursorOverride); // Update visibility based on input enabled state. @@ -147,6 +153,7 @@ private: FImGuiModuleManager* ModuleManager = nullptr; TWeakObjectPtr GameViewport; + TWeakObjectPtr InputHandler; mutable TArray VertexBuffer; mutable TArray IndexBuffer; diff --git a/Source/ImGui/Public/ImGuiInputHandler.h b/Source/ImGui/Public/ImGuiInputHandler.h new file mode 100644 index 0000000..10612a4 --- /dev/null +++ b/Source/ImGui/Public/ImGuiInputHandler.h @@ -0,0 +1,168 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include +#include + +#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 GameViewport; + + int32 ContextIndex = -1; + + friend class FImGuiInputHandlerFactory; +};