Open Unit2Ed opened 5 years ago
We've worked around this temporarily by stopping FWindowsApplication()
from creating the TextInputMethodSystem
and adding a subsequently missing call to DefWindowProc
in FWindowsApplication::ProcessDeferredMessage
. Not pretty, but we don't rely on Unreal for any other text fields, and the IME still triggers anyway for Slate controls (but is positioned incorrectly).
@Unit2Ed I tried to implement ITextInputMethodContext and it works well.
I disable the default ContextProxy
(used in PIE runtime) and create a new one, because I need the ImGui to run in EDITOR only.
// ImGuiTextInputMethodContext.h
#pragma once
class SImGuiWidgetEd;
class FImGuiTextInputMethodContext : public ITextInputMethodContext
{
public:
static TSharedRef<FImGuiTextInputMethodContext> Create(const TSharedRef<SImGuiWidgetEd>& Widget);
void CacheWindow();
virtual bool IsComposing() override;
virtual bool IsReadOnly() override;
virtual uint32 GetTextLength() override;
virtual void GetSelectionRange(uint32& BeginIndex, uint32& Length, ECaretPosition& CaretPosition) override;
virtual void SetSelectionRange(const uint32 BeginIndex, const uint32 Length, const ECaretPosition CaretPosition) override;
virtual void GetTextInRange(const uint32 BeginIndex, const uint32 Length, FString& OutString) override;
virtual void SetTextInRange(const uint32 BeginIndex, const uint32 Length, const FString& InString) override;
virtual int32 GetCharacterIndexFromPoint(const FVector2D& Point) override;
virtual bool GetTextBounds(const uint32 BeginIndex, const uint32 Length, FVector2D& Position, FVector2D& Size) override;
virtual void GetScreenBounds(FVector2D& Position, FVector2D& Size) override;
virtual TSharedPtr<FGenericWindow> GetWindow() override;
virtual void BeginComposition() override;
virtual void UpdateCompositionRange(const int32 InBeginIndex, const uint32 InLength) override;
virtual void EndComposition() override;
private:
FImGuiTextInputMethodContext(const TSharedRef<SImGuiWidgetEd>& Widget);
TWeakPtr<SImGuiWidgetEd> OwnerWidget;
TWeakPtr<SWindow> CachedParentWindow;
bool bIsComposing;
int32 CompositionBeginIndex;
uint32 CompositionLength;
uint32 SelectionRangeBeginIndex;
uint32 SelectionRangeLength;
ECaretPosition SelectionCaretPosition;
FString CompositionString;
};
// ImGuiTextInputMethodContext.cpp
#include "ImGuiTextInputMethodContext.h"
#include "imgui_internal.h"
TSharedRef<FImGuiTextInputMethodContext> FImGuiTextInputMethodContext::Create(const TSharedRef<SImGuiWidgetEd>& Widget)
{
return MakeShareable(new FImGuiTextInputMethodContext(Widget));
}
void FImGuiTextInputMethodContext::CacheWindow()
{
const TSharedRef<const SWidget> OwningSlateWidgetPtr = OwnerWidget.Pin().ToSharedRef();
CachedParentWindow = FSlateApplication::Get().FindWidgetWindow(OwningSlateWidgetPtr);
}
bool FImGuiTextInputMethodContext::IsComposing()
{
return bIsComposing;
}
bool FImGuiTextInputMethodContext::IsReadOnly()
{
return false;
}
uint32 FImGuiTextInputMethodContext::GetTextLength()
{
return CompositionString.Len();
}
void FImGuiTextInputMethodContext::GetSelectionRange(uint32& BeginIndex, uint32& Length, ECaretPosition& CaretPosition)
{
BeginIndex = SelectionRangeBeginIndex;
Length = SelectionRangeLength;
CaretPosition = SelectionCaretPosition;
}
void FImGuiTextInputMethodContext::SetSelectionRange(const uint32 BeginIndex, const uint32 Length,
const ECaretPosition CaretPosition)
{
SelectionRangeBeginIndex = BeginIndex;
SelectionRangeLength = Length;
SelectionCaretPosition = CaretPosition;
}
void FImGuiTextInputMethodContext::GetTextInRange(const uint32 BeginIndex, const uint32 Length, FString& OutString)
{
OutString = CompositionString.Mid(BeginIndex, Length);
}
void FImGuiTextInputMethodContext::SetTextInRange(const uint32 BeginIndex, const uint32 Length, const FString& InString)
{
FString NewString;
if (BeginIndex > 0)
{
NewString = CompositionString.Mid(0, BeginIndex);
}
NewString += InString;
if ((int32)(BeginIndex + Length) < CompositionString.Len())
{
NewString += CompositionString.Mid(BeginIndex + Length, CompositionString.Len() - (BeginIndex + Length));
}
CompositionString = NewString;
// UE_LOG(LogUnrealImGui, Log, TEXT("SetTextInRange BeginIndex = %d, Length = %d, InString = %s, newString = %s"), BeginIndex, Length, *InString, *CompositionString);
}
int32 FImGuiTextInputMethodContext::GetCharacterIndexFromPoint(const FVector2D& Point)
{
int32 ResultIdx = INDEX_NONE;
return ResultIdx;
}
bool FImGuiTextInputMethodContext::GetTextBounds(const uint32 BeginIndex, const uint32 Length, FVector2D& Position, FVector2D& Size)
{
if (OwnerWidget.IsValid())
{
ImGuiContext* ImGuiContext = ImGui::GetCurrentContext();
if (ImGuiContext)
{
// Let the IME editor follow the cursor
Position = FVector2D(CachedGeometry.AbsolutePosition.X + ImGuiContext->PlatformImeLastPos.x,
CachedGeometry.AbsolutePosition.Y + ImGuiContext->PlatformImeLastPos.y + 20);
}
}
return false;
}
void FImGuiTextInputMethodContext::GetScreenBounds(FVector2D& Position, FVector2D& Size)
{
}
TSharedPtr<FGenericWindow> FImGuiTextInputMethodContext::GetWindow()
{
const TSharedPtr<SWindow> SlateWindow = CachedParentWindow.Pin();
return SlateWindow.IsValid() ? SlateWindow->GetNativeWindow() : nullptr;
}
void FImGuiTextInputMethodContext::BeginComposition()
{
if (!bIsComposing)
{
bIsComposing = true;
}
}
void FImGuiTextInputMethodContext::UpdateCompositionRange(const int32 InBeginIndex, const uint32 InLength)
{
CompositionBeginIndex = InBeginIndex;
CompositionLength = InLength;
}
void FImGuiTextInputMethodContext::EndComposition()
{
if (bIsComposing)
{
bIsComposing = false;
if (OwnerWidget.IsValid())
{
ImGuiContext* ImGuiContext = ImGui::GetCurrentContext();
ImGuiID ID = OwnerWidget.Pin()->CurrentActiveInputTextID;
// UE_LOG(LogUnrealImGui, Log, TEXT("EndComposition, set ID = %d CompositionString = %s"), ID, *CompositionString);
if (ImGuiContext->InputTextState.ID == ID)
{
auto CharArray = CompositionString.GetCharArray();
for (int i = 0; i < CharArray.Num(); i++)
{
OwnerWidget.Pin()->AddCharacter(CharArray[i]);
}
CompositionString.Empty();
CompositionBeginIndex = 0;
CompositionLength = 0;
SelectionRangeBeginIndex = 0;
SelectionRangeLength = 0;
}
}
}
}
FImGuiTextInputMethodContext::FImGuiTextInputMethodContext(const TSharedRef<SImGuiWidgetEd>& Widget)
: OwnerWidget(Widget)
, bIsComposing(false)
, CompositionBeginIndex(0)
, CompositionLength(0)
, SelectionRangeBeginIndex(0)
, SelectionRangeLength(0)
, SelectionCaretPosition(ECaretPosition::Beginning)
{
}
Register the context in your ImGuiWidget
ITextInputMethodSystem* const TextInputMethodSystem = FSlateApplication::Get().GetTextInputMethodSystem();
if (TextInputMethodSystem)
{
if (!bHasRegisteredTextInputMethodContext)
{
bHasRegisteredTextInputMethodContext = true;
TextInputMethodChangeNotifier = TextInputMethodSystem->RegisterContext(TextInputMethodContext.ToSharedRef());
if (TextInputMethodChangeNotifier.IsValid())
{
TextInputMethodChangeNotifier->NotifyLayoutChanged(ITextInputMethodChangeNotifier::ELayoutChangeType::Created);
}
}
TextInputMethodContext->CacheWindow();
TextInputMethodSystem->ActivateContext(TextInputMethodContext.ToSharedRef());
}
Note that I use the AddCharacter
API to convert TCHAR
to ImGui character.
void SImGuiWidgetEd::AddCharacter(TCHAR ch)
{
if (InputHandler.IsValid() && InputHandler->GetInputState())
{
InputHandler->GetInputState()->AddCharacter(ch);
}
}
Result:
It appears that the plugin drops support for IME, which ImGui has rudimentary support for. Unreal seems to conflict with ImGui's use of
::ImmGetContext
, causing it to be deactivated, except when using a slate text widget.It looks like the engine would want us to implement ITextInputMethodContext for SImGuiWidget, but it seems to be quite involved - requiring a much more complex interaction with ImGui to pull out the active document, render the composition etc.
Has anyone tried doing this, or found a way for Unreal's TextInputService to co-exist with the simple
::ImmGetContext
API?