Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IME Support for WindowsDX build #537

Merged
merged 36 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b865f27
upgrade RampastringXNAUI Version
frg2089 Jan 22, 2025
6fe7ef1
Add CJK IME Support
frg2089 Jan 23, 2025
3e3b3c2
fixed some error
frg2089 Jan 23, 2025
d4e37a7
Refactor IME handler to align with XNAUI PR 36
SadPencil Jan 23, 2025
4f1ea9e
Move IMEHandler to ClientGUI
SadPencil Jan 23, 2025
3e36b5d
Update IME handler
SadPencil Jan 24, 2025
d8038a6
Update IME
SadPencil Jan 24, 2025
47540c6
Update IMEHandler.cs
SadPencil Jan 24, 2025
05fec2c
Update IME handler
SadPencil Jan 24, 2025
0f09f5c
Update IMEHandler.cs
SadPencil Jan 24, 2025
0b8db0e
Update IMEHandler.cs
SadPencil Jan 24, 2025
d876b03
Update IMEHandler.cs
SadPencil Jan 24, 2025
088b4b3
Update IMEHandler.cs
SadPencil Jan 24, 2025
ce86185
Update IMEHandler.cs
SadPencil Jan 24, 2025
6e7fed8
Update IMEHandler.cs
SadPencil Jan 24, 2025
a15d6d8
Update SdlIMEHandler.cs
SadPencil Jan 24, 2025
4230c30
Code style updates
SadPencil Jan 24, 2025
3edd6e9
Update IMEHandler.cs
SadPencil Jan 24, 2025
852d474
Update IMEHandler.cs
SadPencil Jan 24, 2025
4b2ecde
Update IMEHandler.cs
SadPencil Jan 24, 2025
e893258
Update IMEHandler.cs
SadPencil Jan 24, 2025
f5941a4
Upgrade to a temp build of XNAUI
SadPencil Jan 24, 2025
116318a
Disable IME for XNA builds
SadPencil Jan 24, 2025
7b653c3
Upgrade to XNAUI 2.5.1
SadPencil Jan 28, 2025
5ae18ca
Update comments for disabling IME for XNA builds
SadPencil Jan 28, 2025
6005f53
Make `SetTextInputRectangle` abstract
SadPencil Feb 1, 2025
10a5a40
Apply suggestions from code review
SadPencil Feb 2, 2025
d216b0a
Update IMEHandler.cs
SadPencil Feb 2, 2025
db6cb29
Merge branch 'feat/ime-preview' of https://github.com/frg2089/xna-cnc…
SadPencil Feb 2, 2025
7c52ef5
Comment out Debug.WriteLine
SadPencil Feb 2, 2025
1210268
Disable IME for CnCNet username
SadPencil Feb 2, 2025
7b6a818
Update IMEHandler.cs
SadPencil Feb 2, 2025
b3fa88e
SetIMETextInputRectangle on window resizing
SadPencil Feb 2, 2025
6b6b65d
Update IMEHandler.cs
SadPencil Feb 2, 2025
6cb294d
Mark new files nullable
SadPencil Feb 3, 2025
b7b4217
Update WinFormsIMEHandler.cs
SadPencil Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading