Skip to content

Commit

Permalink
IME Support for WindowsDX build (#537)
Browse files Browse the repository at this point in the history
* upgrade RampastringXNAUI Version

Signed-off-by: 舰队的偶像-岛风酱! <[email protected]>

* Add CJK IME Support

DX  XNA (WinForms base) Only
OGL UGL (SDL base) are not support

Signed-off-by: 舰队的偶像-岛风酱! <[email protected]>

* fixed some error

* Refactor IME handler to align with XNAUI PR 36

* Move IMEHandler to ClientGUI

* Update IME handler

* Update IME

* Update IMEHandler.cs

* Update IME handler

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update SdlIMEHandler.cs

* Code style updates

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Update IMEHandler.cs

* Upgrade to a temp build of XNAUI

* Disable IME for XNA builds

* Upgrade to XNAUI 2.5.1

* Update comments for disabling IME for XNA builds

* Make `SetTextInputRectangle` abstract

* Apply suggestions from code review

Co-authored-by: Kerbiter <[email protected]>

* Update IMEHandler.cs

* Comment out Debug.WriteLine

* Disable IME for CnCNet username

* Update IMEHandler.cs

* SetIMETextInputRectangle on window resizing

* Update IMEHandler.cs

* Mark new files nullable

* Update WinFormsIMEHandler.cs

---------

Signed-off-by: 舰队的偶像-岛风酱! <[email protected]>
Co-authored-by: SadPencil <[email protected]>
Co-authored-by: Kerbiter <[email protected]>
  • Loading branch information
3 people authored Feb 3, 2025
1 parent 3c0d2e0 commit 7c02b0d
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 10 deletions.
11 changes: 11 additions & 0 deletions ClientGUI/ClientGUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@
<ItemGroup>
<ProjectReference Include="..\ClientCore\ClientCore.csproj" />
</ItemGroup>
<ItemGroup Condition="$(Configuration.Contains('GL'))">
<!--Remove WinForm-->
<Compile Remove="IME\WinFormsIMEHandler.cs" />
<None Include="IME\WinFormsIMEHandler.cs" />
</ItemGroup>
<ItemGroup Condition="!$(Configuration.Contains('GL'))">
<!--Remove SDL-->
<Compile Remove="IME\SdlIMEHandler.cs" />
<None Include="IME\SdlIMEHandler.cs" />
<PackageReference Include="ImeSharp" />
</ItemGroup>
</Project>
16 changes: 16 additions & 0 deletions ClientGUI/IME/DummyIMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#nullable enable
using Microsoft.Xna.Framework;

namespace ClientGUI.IME
{
internal class DummyIMEHandler : IMEHandler
{
public DummyIMEHandler() { }

public override bool TextCompositionEnabled { get => false; protected set { } }

public override void SetTextInputRectangle(Rectangle rectangle) { }
public override void StartTextComposition() { }
public override void StopTextComposition() { }
}
}
226 changes: 226 additions & 0 deletions ClientGUI/IME/IMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Diagnostics;

using Microsoft.Xna.Framework;

using Rampastring.XNAUI;
using Rampastring.XNAUI.Input;
using Rampastring.XNAUI.XNAControls;

namespace ClientGUI.IME;
public abstract class IMEHandler : IIMEHandler
{
bool IIMEHandler.TextCompositionEnabled => TextCompositionEnabled;
public abstract bool TextCompositionEnabled { get; protected set; }

private XNATextBox? _IMEFocus = null;
public XNATextBox? IMEFocus
{
get => _IMEFocus;
protected set
{
_IMEFocus = value;
Debug.Assert(!_IMEFocus?.IMEDisabled ?? true, "IME focus should not be assigned from a textbox with IME disabled");
}
}

private string _composition = string.Empty;

public string Composition
{
get => _composition;
protected set
{
string old = _composition;
_composition = value;
OnCompositionChanged(old, value);
}
}

public bool CompositionEmpty => string.IsNullOrEmpty(_composition);

protected bool IMEEventReceived = false;
protected bool LastActionIMEChatInput = true;

private void OnCompositionChanged(string oldValue, string newValue)
{
//Debug.WriteLine($"IME: OnCompositionChanged: {newValue.Length - oldValue.Length}");

IMEEventReceived = true;
// It seems that OnIMETextInput() is always triggered after OnCompositionChanged(). We expect such a behavior.
LastActionIMEChatInput = false;
}

protected ConcurrentDictionary<XNATextBox, Action<char>?> TextBoxHandleChatInputCallbacks = [];

public virtual int CompositionCursorPosition { get; set; }

public static IMEHandler Create(Game game)
{
#if DX
return new WinFormsIMEHandler(game);
#elif XNA
// Warning: Think carefully before enabling WinFormsIMEHandler for XNA builds!
// It *might* occasionally crash due to an unknown stack overflow issue.
// This *might* be caused by both ImeSharp and XNAUI hooking into WndProc.
// ImeSharp: https://github.com/ryancheung/ImeSharp/blob/dc2243beff9ef48eb37e398c506c905c965f8e68/ImeSharp/InputMethod.cs#L170
// XNAUI: https://github.com/Rampastring/Rampastring.XNAUI/blob/9a7d5bb3e47ea50286ee05073d0a6723bc6d764d/Input/KeyboardEventInput.cs#L79
//
// That said, you can try returning a WinFormsIMEHandler and test if it is stable enough now. Who knows?
return new DummyIMEHandler();
#elif GL
return new SdlIMEHandler(game);
#else
#error Unknown variant
#endif
}

public abstract void SetTextInputRectangle(Rectangle rectangle);

public abstract void StartTextComposition();

public abstract void StopTextComposition();

protected virtual void OnIMETextInput(char character)
{
//Debug.WriteLine($"IME: OnIMETextInput: {character} {(short)character}; IMEFocus is null? {IMEFocus == null}");

IMEEventReceived = true;
LastActionIMEChatInput = true;

if (IMEFocus != null)
{
TextBoxHandleChatInputCallbacks.TryGetValue(IMEFocus, out var handleChatInput);
handleChatInput?.Invoke(character);
}
}

public void SetIMETextInputRectangle(WindowManager manager)
{
// When the client window resizes, we should call SetIMETextInputRectangle()
if (manager.SelectedControl is XNATextBox textBox)
SetIMETextInputRectangle(textBox);
}

private void SetIMETextInputRectangle(XNATextBox sender)
{
WindowManager windowManager = sender.WindowManager;

Rectangle textBoxRect = sender.RenderRectangle();
double scaleRatio = windowManager.ScaleRatio;

Rectangle rect = new()
{
X = (int)(textBoxRect.X * scaleRatio + windowManager.SceneXPosition),
Y = (int)(textBoxRect.Y * scaleRatio + windowManager.SceneYPosition),
Width = (int)(textBoxRect.Width * scaleRatio),
Height = (int)(textBoxRect.Height * scaleRatio)
};

// The following code returns a more accurate location based on the current InputPosition.
// However, as SetIMETextInputRectangle() does not automatically update with changes in InputPosition
// (e.g., due to scrolling or mouse clicks altering the textbox's input position without shifting focus),
// accuracy becomes inconsistent. Sometimes it's precise, other times it's off,
// which is arguably worse than a consistent but manageable inaccuracy.
// This inconsistency could lead to a confusing user experience,
// as the input rectangle's position may not reliably reflect the current input position.
// Therefore, unless whenever InputPosition is changed, SetIMETextInputRectangle() is raised
// -- which requires more time to investigate and test, it's commented out for now.
//var vec = Renderer.GetTextDimensions(
// sender.Text.Substring(sender.TextStartPosition, sender.InputPosition),
// sender.FontIndex);
//rect.X += (int)(vec.X * scaleRatio);

SetTextInputRectangle(rect);
}

void IIMEHandler.OnSelectedChanged(XNATextBox sender)
{
if (sender.WindowManager.SelectedControl == sender)
{
StopTextComposition();

if (!sender.IMEDisabled && sender.Enabled && sender.Visible)
{
IMEFocus = sender;

// Update the location of IME based on the textbox
SetIMETextInputRectangle(sender);

StartTextComposition();
}
else
{
IMEFocus = null;
}
}
else if (sender.WindowManager.SelectedControl is not XNATextBox)
{
// Disable IME since the current selected control is not XNATextBox
IMEFocus = null;
StopTextComposition();
}

// Note: if sender.WindowManager.SelectedControl != sender and is XNATextBox,
// another OnSelectedChanged() will be triggered,
// so we do not need to handle this case
}

void IIMEHandler.RegisterXNATextBox(XNATextBox sender, Action<char>? handleCharInput)
=> TextBoxHandleChatInputCallbacks[sender] = handleCharInput;

void IIMEHandler.KillXNATextBox(XNATextBox sender)
=> TextBoxHandleChatInputCallbacks.TryRemove(sender, out _);

bool IIMEHandler.HandleScrollLeftKey(XNATextBox sender)
=> !CompositionEmpty;

bool IIMEHandler.HandleScrollRightKey(XNATextBox sender)
=> !CompositionEmpty;

bool IIMEHandler.HandleBackspaceKey(XNATextBox sender)
{
bool handled = !LastActionIMEChatInput;
LastActionIMEChatInput = true;
//Debug.WriteLine($"IME: HandleBackspaceKey: handled: {handled}");
return handled;
}

bool IIMEHandler.HandleDeleteKey(XNATextBox sender)
{
bool handled = !LastActionIMEChatInput;
LastActionIMEChatInput = true;
//Debug.WriteLine($"IME: HandleDeleteKey: handled: {handled}");
return handled;
}

bool IIMEHandler.GetDrawCompositionText(XNATextBox sender, out string composition, out int compositionCursorPosition)
{
if (IMEFocus != sender || CompositionEmpty)
{
composition = string.Empty;
compositionCursorPosition = 0;
return false;
}

composition = Composition;
compositionCursorPosition = CompositionCursorPosition;
return true;
}

bool IIMEHandler.HandleCharInput(XNATextBox sender, char input)
=> TextCompositionEnabled;

bool IIMEHandler.HandleEnterKey(XNATextBox sender)
=> false;

bool IIMEHandler.HandleEscapeKey(XNATextBox sender)
{
//Debug.WriteLine($"IME: HandleEscapeKey: handled: {IMEEventReceived}");
return IMEEventReceived;
}

void IIMEHandler.OnTextChanged(XNATextBox sender) { }
}
17 changes: 17 additions & 0 deletions ClientGUI/IME/SdlIMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#nullable enable
using Microsoft.Xna.Framework;

namespace ClientGUI.IME;

/// <summary>
/// Integrate IME to DesktopGL(SDL2) platform.
/// </summary>
/// <remarks>
/// Note: We were unable to provide reliable input method support for
/// SDL2 due to the lack of a way to be able to stabilize hooks for
/// the SDL2 main loop.<br/>
/// Perhaps this requires some changes in Monogame.
/// </remarks>
internal sealed class SdlIMEHandler(Game game) : DummyIMEHandler

Check warning on line 15 in ClientGUI/IME/SdlIMEHandler.cs

View workflow job for this annotation

GitHub Actions / build-clients (Ares)

Parameter 'game' is unread.

Check warning on line 15 in ClientGUI/IME/SdlIMEHandler.cs

View workflow job for this annotation

GitHub Actions / build-clients (TS)

Parameter 'game' is unread.

Check warning on line 15 in ClientGUI/IME/SdlIMEHandler.cs

View workflow job for this annotation

GitHub Actions / build-clients (YR)

Parameter 'game' is unread.
{
}
56 changes: 56 additions & 0 deletions ClientGUI/IME/WinFormsIMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#nullable enable
using System;

using ImeSharp;

using Microsoft.Xna.Framework;

using Rampastring.Tools;

namespace ClientGUI.IME;

/// <summary>
/// Integrate IME to XNA framework.
/// </summary>
internal class WinFormsIMEHandler : IMEHandler
{
public override bool TextCompositionEnabled
{
get => InputMethod.Enabled;
protected set
{
if (value != InputMethod.Enabled)
InputMethod.Enabled = value;
}
}

public WinFormsIMEHandler(Game game)
{
Logger.Log($"Initialize WinFormsIMEHandler.");
if (game?.Window?.Handle == null)
throw new Exception("The handle of game window should not be null");

InputMethod.Initialize(game.Window.Handle);
InputMethod.TextInputCallback = OnIMETextInput;
InputMethod.TextCompositionCallback = (compositionText, cursorPosition) =>
{
Composition = compositionText.ToString();
CompositionCursorPosition = cursorPosition;
};
}

public override void StartTextComposition()
{
//Debug.WriteLine("IME: StartTextComposition");
TextCompositionEnabled = true;
}

public override void StopTextComposition()
{
//Debug.WriteLine("IME: StopTextComposition");
TextCompositionEnabled = false;
}

public override void SetTextInputRectangle(Rectangle rect)
=> InputMethod.SetTextInputRect(rect.X, rect.Y, rect.Width, rect.Height);
}
20 changes: 12 additions & 8 deletions DXMainClient/DXGUI/GameClass.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using ClientCore;
using ClientCore;
using ClientCore.CnCNet5;
using ClientGUI;
using ClientGUI.IME;
using DTAClient.Domain;
using DTAClient.DXGUI.Generic;
using ClientCore.Extensions;
Expand All @@ -10,7 +11,8 @@
using Rampastring.Tools;
using Rampastring.XNAUI;
using System;
using ClientGUI;
using System.Diagnostics;
using System.IO;
using DTAClient.Domain.Multiplayer;
using DTAClient.Domain.Multiplayer.CnCNet;
using DTAClient.DXGUI.Multiplayer;
Expand All @@ -23,13 +25,8 @@
using Microsoft.Extensions.Hosting;
using Rampastring.XNAUI.XNAControls;
using MainMenu = DTAClient.DXGUI.Generic.MainMenu;
#if DX || (GL && WINFORMS)
using System.Diagnostics;
using System.IO;
#endif
#if WINFORMS
using System.Windows.Forms;
using System.IO;
#endif

namespace DTAClient.DXGUI
Expand Down Expand Up @@ -144,8 +141,10 @@ protected override void Initialize()
#endif
InitializeUISettings();

WindowManager wm = new WindowManager(this, graphics);
WindowManager wm = new(this, graphics);
wm.Initialize(content, ProgramConstants.GetBaseResourcePath());
IMEHandler imeHandler = IMEHandler.Create(this);
wm.IMEHandler = imeHandler;

wm.ControlINIAttributeParsers.Add(new TranslationINIParser());

Expand Down Expand Up @@ -192,6 +191,11 @@ protected override void Initialize()
// SetGraphicsMode(wm, currentWindowSize.Width, currentWindowSize.Height, centerOnScreen: false);
// }
//};

wm.WindowSizeChangedByUser += (sender, e) =>
{
imeHandler.SetIMETextInputRectangle(wm);
};
}
#endif

Expand Down
Loading

0 comments on commit 7c02b0d

Please sign in to comment.