UnrealImGui icon indicating copy to clipboard operation
UnrealImGui copied to clipboard

Input Method Editor implementation?

Open Unit2Ed opened this issue 5 years ago • 2 comments

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 avatar Mar 06 '19 14:03 Unit2Ed

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 avatar Mar 11 '19 08:03 Unit2Ed

@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

raytaylorlin avatar Nov 10 '21 03:11 raytaylorlin