segross / UnrealImGui

Unreal plug-in that integrates Dear ImGui framework into Unreal Engine 4.
MIT License
687 stars 217 forks source link

Input Method Editor implementation? #15

Open Unit2Ed opened 5 years ago

Unit2Ed commented 5 years ago

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?

Unit2Ed commented 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).

raytaylorlin commented 2 years ago

@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:

image