395 lines
12 KiB
C#
395 lines
12 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace Microsoft.Maui.Platform.Linux.Services;
|
|
|
|
/// <summary>
|
|
/// X11 Input Method service using XIM protocol.
|
|
/// Provides IME support for CJK and other complex input methods.
|
|
/// </summary>
|
|
public class X11InputMethodService : IInputMethodService, IDisposable
|
|
{
|
|
private nint _display;
|
|
private nint _window;
|
|
private nint _xim;
|
|
private nint _xic;
|
|
private IInputContext? _currentContext;
|
|
private string _preEditText = string.Empty;
|
|
private int _preEditCursorPosition;
|
|
private bool _isActive;
|
|
private bool _disposed;
|
|
|
|
// XIM callback delegates (prevent GC)
|
|
private XIMProc? _preeditStartCallback;
|
|
private XIMProc? _preeditDoneCallback;
|
|
private XIMProc? _preeditDrawCallback;
|
|
private XIMProc? _preeditCaretCallback;
|
|
private XIMProc? _commitCallback;
|
|
|
|
public bool IsActive => _isActive;
|
|
public string PreEditText => _preEditText;
|
|
public int PreEditCursorPosition => _preEditCursorPosition;
|
|
|
|
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
|
|
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
|
|
public event EventHandler? PreEditEnded;
|
|
|
|
public void Initialize(nint windowHandle)
|
|
{
|
|
_window = windowHandle;
|
|
|
|
// Get display from X11 interop
|
|
_display = XOpenDisplay(IntPtr.Zero);
|
|
if (_display == IntPtr.Zero)
|
|
{
|
|
Console.WriteLine("X11InputMethodService: Failed to open display");
|
|
return;
|
|
}
|
|
|
|
// Set locale for proper IME operation
|
|
if (XSetLocaleModifiers("") == IntPtr.Zero)
|
|
{
|
|
XSetLocaleModifiers("@im=none");
|
|
}
|
|
|
|
// Open input method
|
|
_xim = XOpenIM(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
|
if (_xim == IntPtr.Zero)
|
|
{
|
|
Console.WriteLine("X11InputMethodService: No input method available, trying IBus...");
|
|
TryIBusFallback();
|
|
return;
|
|
}
|
|
|
|
CreateInputContext();
|
|
}
|
|
|
|
private void CreateInputContext()
|
|
{
|
|
if (_xim == IntPtr.Zero || _window == IntPtr.Zero) return;
|
|
|
|
// Create input context with preedit callbacks
|
|
var preeditAttr = CreatePreeditAttributes();
|
|
|
|
_xic = XCreateIC(_xim,
|
|
XNClientWindow, _window,
|
|
XNFocusWindow, _window,
|
|
XNInputStyle, XIMPreeditCallbacks | XIMStatusNothing,
|
|
XNPreeditAttributes, preeditAttr,
|
|
IntPtr.Zero);
|
|
|
|
if (preeditAttr != IntPtr.Zero)
|
|
{
|
|
XFree(preeditAttr);
|
|
}
|
|
|
|
if (_xic == IntPtr.Zero)
|
|
{
|
|
// Fallback to simpler input style
|
|
_xic = XCreateICSimple(_xim,
|
|
XNClientWindow, _window,
|
|
XNFocusWindow, _window,
|
|
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
|
|
IntPtr.Zero);
|
|
}
|
|
|
|
if (_xic != IntPtr.Zero)
|
|
{
|
|
Console.WriteLine("X11InputMethodService: Input context created successfully");
|
|
}
|
|
}
|
|
|
|
private nint CreatePreeditAttributes()
|
|
{
|
|
// Set up preedit callbacks for on-the-spot composition
|
|
_preeditStartCallback = PreeditStartCallback;
|
|
_preeditDoneCallback = PreeditDoneCallback;
|
|
_preeditDrawCallback = PreeditDrawCallback;
|
|
_preeditCaretCallback = PreeditCaretCallback;
|
|
|
|
// Create callback structures
|
|
// Note: Actual implementation would marshal XIMCallback structures
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
private int PreeditStartCallback(nint xic, nint clientData, nint callData)
|
|
{
|
|
_isActive = true;
|
|
_preEditText = string.Empty;
|
|
_preEditCursorPosition = 0;
|
|
return -1; // No length limit
|
|
}
|
|
|
|
private int PreeditDoneCallback(nint xic, nint clientData, nint callData)
|
|
{
|
|
_isActive = false;
|
|
_preEditText = string.Empty;
|
|
_preEditCursorPosition = 0;
|
|
PreEditEnded?.Invoke(this, EventArgs.Empty);
|
|
_currentContext?.OnPreEditEnded();
|
|
return 0;
|
|
}
|
|
|
|
private int PreeditDrawCallback(nint xic, nint clientData, nint callData)
|
|
{
|
|
// Parse XIMPreeditDrawCallbackStruct
|
|
// Update preedit text and cursor position
|
|
// This would involve marshaling the callback data structure
|
|
|
|
PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition));
|
|
_currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition);
|
|
return 0;
|
|
}
|
|
|
|
private int PreeditCaretCallback(nint xic, nint clientData, nint callData)
|
|
{
|
|
// Handle caret movement in preedit text
|
|
return 0;
|
|
}
|
|
|
|
private void TryIBusFallback()
|
|
{
|
|
// Try to connect to IBus via D-Bus
|
|
// This provides a more modern IME interface
|
|
Console.WriteLine("X11InputMethodService: IBus fallback not yet implemented");
|
|
}
|
|
|
|
public void SetFocus(IInputContext? context)
|
|
{
|
|
_currentContext = context;
|
|
|
|
if (_xic != IntPtr.Zero)
|
|
{
|
|
if (context != null)
|
|
{
|
|
XSetICFocus(_xic);
|
|
}
|
|
else
|
|
{
|
|
XUnsetICFocus(_xic);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetCursorLocation(int x, int y, int width, int height)
|
|
{
|
|
if (_xic == IntPtr.Zero) return;
|
|
|
|
// Set the spot location for candidate window positioning
|
|
var spotLocation = new XPoint { x = (short)x, y = (short)y };
|
|
|
|
var attr = XVaCreateNestedList(0,
|
|
XNSpotLocation, ref spotLocation,
|
|
IntPtr.Zero);
|
|
|
|
if (attr != IntPtr.Zero)
|
|
{
|
|
XSetICValues(_xic, XNPreeditAttributes, attr, IntPtr.Zero);
|
|
XFree(attr);
|
|
}
|
|
}
|
|
|
|
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
|
|
{
|
|
if (_xic == IntPtr.Zero) return false;
|
|
|
|
// Convert to X11 key event
|
|
var xEvent = new XKeyEvent
|
|
{
|
|
type = isKeyDown ? KeyPress : KeyRelease,
|
|
display = _display,
|
|
window = _window,
|
|
state = ConvertModifiers(modifiers),
|
|
keycode = keyCode
|
|
};
|
|
|
|
// Filter through XIM
|
|
if (XFilterEvent(ref xEvent, _window))
|
|
{
|
|
return true; // Event consumed by IME
|
|
}
|
|
|
|
// If not filtered and key down, try to get committed text
|
|
if (isKeyDown)
|
|
{
|
|
var buffer = new byte[64];
|
|
var keySym = IntPtr.Zero;
|
|
var status = IntPtr.Zero;
|
|
|
|
int len = Xutf8LookupString(_xic, ref xEvent, buffer, buffer.Length, ref keySym, ref status);
|
|
|
|
if (len > 0)
|
|
{
|
|
string text = Encoding.UTF8.GetString(buffer, 0, len);
|
|
OnTextCommit(text);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void OnTextCommit(string text)
|
|
{
|
|
_preEditText = string.Empty;
|
|
_preEditCursorPosition = 0;
|
|
|
|
TextCommitted?.Invoke(this, new TextCommittedEventArgs(text));
|
|
_currentContext?.OnTextCommitted(text);
|
|
}
|
|
|
|
private uint ConvertModifiers(KeyModifiers modifiers)
|
|
{
|
|
uint state = 0;
|
|
if (modifiers.HasFlag(KeyModifiers.Shift)) state |= ShiftMask;
|
|
if (modifiers.HasFlag(KeyModifiers.Control)) state |= ControlMask;
|
|
if (modifiers.HasFlag(KeyModifiers.Alt)) state |= Mod1Mask;
|
|
if (modifiers.HasFlag(KeyModifiers.Super)) state |= Mod4Mask;
|
|
if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= LockMask;
|
|
if (modifiers.HasFlag(KeyModifiers.NumLock)) state |= Mod2Mask;
|
|
return state;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
if (_xic != IntPtr.Zero)
|
|
{
|
|
XmbResetIC(_xic);
|
|
}
|
|
|
|
_preEditText = string.Empty;
|
|
_preEditCursorPosition = 0;
|
|
_isActive = false;
|
|
|
|
PreEditEnded?.Invoke(this, EventArgs.Empty);
|
|
_currentContext?.OnPreEditEnded();
|
|
}
|
|
|
|
public void Shutdown()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
if (_xic != IntPtr.Zero)
|
|
{
|
|
XDestroyIC(_xic);
|
|
_xic = IntPtr.Zero;
|
|
}
|
|
|
|
if (_xim != IntPtr.Zero)
|
|
{
|
|
XCloseIM(_xim);
|
|
_xim = IntPtr.Zero;
|
|
}
|
|
|
|
// Note: Don't close display here if shared with window
|
|
}
|
|
|
|
#region X11 Interop
|
|
|
|
private const int KeyPress = 2;
|
|
private const int KeyRelease = 3;
|
|
|
|
private const uint ShiftMask = 1 << 0;
|
|
private const uint LockMask = 1 << 1;
|
|
private const uint ControlMask = 1 << 2;
|
|
private const uint Mod1Mask = 1 << 3; // Alt
|
|
private const uint Mod2Mask = 1 << 4; // NumLock
|
|
private const uint Mod4Mask = 1 << 6; // Super
|
|
|
|
private const long XIMPreeditNothing = 0x0008L;
|
|
private const long XIMPreeditCallbacks = 0x0002L;
|
|
private const long XIMStatusNothing = 0x0400L;
|
|
|
|
private static readonly nint XNClientWindow = Marshal.StringToHGlobalAnsi("clientWindow");
|
|
private static readonly nint XNFocusWindow = Marshal.StringToHGlobalAnsi("focusWindow");
|
|
private static readonly nint XNInputStyle = Marshal.StringToHGlobalAnsi("inputStyle");
|
|
private static readonly nint XNPreeditAttributes = Marshal.StringToHGlobalAnsi("preeditAttributes");
|
|
private static readonly nint XNSpotLocation = Marshal.StringToHGlobalAnsi("spotLocation");
|
|
|
|
private delegate int XIMProc(nint xic, nint clientData, nint callData);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct XPoint
|
|
{
|
|
public short x;
|
|
public short y;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct XKeyEvent
|
|
{
|
|
public int type;
|
|
public ulong serial;
|
|
public bool send_event;
|
|
public nint display;
|
|
public nint window;
|
|
public nint root;
|
|
public nint subwindow;
|
|
public ulong time;
|
|
public int x, y;
|
|
public int x_root, y_root;
|
|
public uint state;
|
|
public uint keycode;
|
|
public bool same_screen;
|
|
}
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XOpenDisplay(nint display);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XSetLocaleModifiers(string modifiers);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XOpenIM(nint display, nint db, nint res_name, nint res_class);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XCloseIM(nint xim);
|
|
|
|
[DllImport("libX11.so.6", EntryPoint = "XCreateIC")]
|
|
private static extern nint XCreateIC(nint xim, nint name1, nint value1, nint name2, nint value2,
|
|
nint name3, long value3, nint name4, nint value4, nint terminator);
|
|
|
|
[DllImport("libX11.so.6", EntryPoint = "XCreateIC")]
|
|
private static extern nint XCreateICSimple(nint xim, nint name1, nint value1, nint name2, nint value2,
|
|
nint name3, long value3, nint terminator);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XDestroyIC(nint xic);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XSetICFocus(nint xic);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XUnsetICFocus(nint xic);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XSetICValues(nint xic, nint name, nint value, nint terminator);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XVaCreateNestedList(int unused, nint name, ref XPoint value, nint terminator);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern bool XFilterEvent(ref XKeyEvent xevent, nint window);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int Xutf8LookupString(nint xic, ref XKeyEvent xevent,
|
|
byte[] buffer, int bytes, ref nint keySym, ref nint status);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XmbResetIC(nint xic);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XFree(nint ptr);
|
|
|
|
#endregion
|
|
}
|