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

Draft
wants to merge 23 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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>
17 changes: 17 additions & 0 deletions ClientGUI/IME/DummyIMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ClientGUI.IME
{
internal class DummyIMEHandler : IMEHandler
{
public DummyIMEHandler() { }

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

public override void StartTextComposition()
{
}

public override void StopTextComposition()
{
}
}
}
215 changes: 215 additions & 0 deletions ClientGUI/IME/IMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#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: do not enable WinFormsIMEHandler for XNA builds!
// Occasionally it could crash for an unknown stack overflow.
// It *might* be due to both ImeSharp and XNAUI hook 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
return new DummyIMEHandler();
#elif GL
return new SdlIMEHandler(game);
#else
#error Unknown variant
#endif
}

public virtual 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);
}
}

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, aware of the input cursor
// However, it requires SetIMETextInputRectangle() be called whenever InputPosition is changed.
// And therefore, it's commented out for now.
//var vec = Renderer.GetTextDimensions(
// sender.Text.Substring(sender.TextStartPosition, sender.TextEndPosition - 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)
{
}
}
16 changes: 16 additions & 0 deletions ClientGUI/IME/SdlIMEHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 14 in ClientGUI/IME/SdlIMEHandler.cs

View workflow job for this annotation

GitHub Actions / build-clients (Ares)

Parameter 'game' is unread.

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

View workflow job for this annotation

GitHub Actions / build-clients (TS)

Parameter 'game' is unread.

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

View workflow job for this annotation

GitHub Actions / build-clients (YR)

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

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.");
Debug.Assert(game?.Window?.Handle != null, "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);
}
14 changes: 6 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,9 @@ protected override void Initialize()
#endif
InitializeUISettings();

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

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

Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RampastringXNAUIVersion>2.3.22</RampastringXNAUIVersion>
<RampastringXNAUIVersion>2.5.0-private-ime-766750d</RampastringXNAUIVersion>
<DotnetLibrariesVersion>8.0.0</DotnetLibrariesVersion>
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="GitVersion.MsBuild" Version="5.12.0" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageVersion Include="ImeSharp" Version="1.4.0" />
<PackageVersion Include="lzo.net" Version="0.0.6" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
Expand Down Expand Up @@ -51,7 +52,6 @@
<!-- and -p:Engine=WindowsDX -f net48 -->
<PackageReference Include="NETStandard.Library" />
<PackageVersion Include="NETStandard.Library" Version="2.0.3" />

<PackageReference Include="System.IO.FileSystem" />
<PackageVersion Include="System.IO.FileSystem" Version="4.3.0" />
</ItemGroup>
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading