-
-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IME Support for WindowsDX build (#537)
* 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
1 parent
3c0d2e0
commit 7c02b0d
Showing
8 changed files
with
341 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / build-clients (Ares)
Check warning on line 15 in ClientGUI/IME/SdlIMEHandler.cs GitHub Actions / build-clients (TS)
|
||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.