// 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;
///
/// IBus Input Method service using D-Bus interface.
/// Provides modern IME support on Linux desktops.
///
public class IBusInputMethodService : IInputMethodService, IDisposable
{
private nint _bus;
private nint _context;
private IInputContext? _currentContext;
private string _preEditText = string.Empty;
private int _preEditCursorPosition;
private bool _isActive;
private bool _disposed;
// Callback delegates (prevent GC)
private IBusCommitTextCallback? _commitCallback;
private IBusUpdatePreeditTextCallback? _preeditCallback;
private IBusShowPreeditTextCallback? _showPreeditCallback;
private IBusHidePreeditTextCallback? _hidePreeditCallback;
public bool IsActive => _isActive;
public string PreEditText => _preEditText;
public int PreEditCursorPosition => _preEditCursorPosition;
public event EventHandler? TextCommitted;
public event EventHandler? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(nint windowHandle)
{
try
{
// Initialize IBus
ibus_init();
// Get the IBus bus connection
_bus = ibus_bus_new();
if (_bus == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to connect to IBus");
return;
}
// Check if IBus is connected
if (!ibus_bus_is_connected(_bus))
{
Console.WriteLine("IBusInputMethodService: IBus not connected");
return;
}
// Create input context
_context = ibus_bus_create_input_context(_bus, "maui-linux");
if (_context == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to create input context");
return;
}
// Set capabilities
uint capabilities = IBUS_CAP_PREEDIT_TEXT | IBUS_CAP_FOCUS | IBUS_CAP_SURROUNDING_TEXT;
ibus_input_context_set_capabilities(_context, capabilities);
// Connect signals
ConnectSignals();
Console.WriteLine("IBusInputMethodService: Initialized successfully");
}
catch (Exception ex)
{
Console.WriteLine($"IBusInputMethodService: Initialization failed - {ex.Message}");
}
}
private void ConnectSignals()
{
if (_context == IntPtr.Zero) return;
// Set up callbacks for IBus signals
_commitCallback = OnCommitText;
_preeditCallback = OnUpdatePreeditText;
_showPreeditCallback = OnShowPreeditText;
_hidePreeditCallback = OnHidePreeditText;
// Connect to commit-text signal
g_signal_connect(_context, "commit-text",
Marshal.GetFunctionPointerForDelegate(_commitCallback), IntPtr.Zero);
// Connect to update-preedit-text signal
g_signal_connect(_context, "update-preedit-text",
Marshal.GetFunctionPointerForDelegate(_preeditCallback), IntPtr.Zero);
// Connect to show-preedit-text signal
g_signal_connect(_context, "show-preedit-text",
Marshal.GetFunctionPointerForDelegate(_showPreeditCallback), IntPtr.Zero);
// Connect to hide-preedit-text signal
g_signal_connect(_context, "hide-preedit-text",
Marshal.GetFunctionPointerForDelegate(_hidePreeditCallback), IntPtr.Zero);
}
private void OnCommitText(nint context, nint text, nint userData)
{
if (text == IntPtr.Zero) return;
string committedText = GetIBusTextString(text);
if (!string.IsNullOrEmpty(committedText))
{
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
TextCommitted?.Invoke(this, new TextCommittedEventArgs(committedText));
_currentContext?.OnTextCommitted(committedText);
}
}
private void OnUpdatePreeditText(nint context, nint text, uint cursorPos, bool visible, nint userData)
{
if (!visible)
{
OnHidePreeditText(context, userData);
return;
}
_isActive = true;
_preEditText = text != IntPtr.Zero ? GetIBusTextString(text) : string.Empty;
_preEditCursorPosition = (int)cursorPos;
var attributes = GetPreeditAttributes(text);
PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, attributes));
_currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition);
}
private void OnShowPreeditText(nint context, nint userData)
{
_isActive = true;
}
private void OnHidePreeditText(nint context, nint userData)
{
_isActive = false;
_preEditText = string.Empty;
_preEditCursorPosition = 0;
PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
private string GetIBusTextString(nint ibusText)
{
if (ibusText == IntPtr.Zero) return string.Empty;
nint textPtr = ibus_text_get_text(ibusText);
if (textPtr == IntPtr.Zero) return string.Empty;
return Marshal.PtrToStringUTF8(textPtr) ?? string.Empty;
}
private List GetPreeditAttributes(nint ibusText)
{
var attributes = new List();
if (ibusText == IntPtr.Zero) return attributes;
nint attrList = ibus_text_get_attributes(ibusText);
if (attrList == IntPtr.Zero) return attributes;
uint count = ibus_attr_list_size(attrList);
for (uint i = 0; i < count; i++)
{
nint attr = ibus_attr_list_get(attrList, i);
if (attr == IntPtr.Zero) continue;
var type = ibus_attribute_get_attr_type(attr);
var start = ibus_attribute_get_start_index(attr);
var end = ibus_attribute_get_end_index(attr);
attributes.Add(new PreEditAttribute
{
Start = (int)start,
Length = (int)(end - start),
Type = ConvertAttributeType(type)
});
}
return attributes;
}
private PreEditAttributeType ConvertAttributeType(uint ibusType)
{
return ibusType switch
{
IBUS_ATTR_TYPE_UNDERLINE => PreEditAttributeType.Underline,
IBUS_ATTR_TYPE_FOREGROUND => PreEditAttributeType.Highlighted,
IBUS_ATTR_TYPE_BACKGROUND => PreEditAttributeType.Reverse,
_ => PreEditAttributeType.None
};
}
public void SetFocus(IInputContext? context)
{
_currentContext = context;
if (_context != IntPtr.Zero)
{
if (context != null)
{
ibus_input_context_focus_in(_context);
}
else
{
ibus_input_context_focus_out(_context);
}
}
}
public void SetCursorLocation(int x, int y, int width, int height)
{
if (_context == IntPtr.Zero) return;
ibus_input_context_set_cursor_location(_context, x, y, width, height);
}
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
{
if (_context == IntPtr.Zero) return false;
uint state = ConvertModifiers(modifiers);
if (!isKeyDown)
{
state |= IBUS_RELEASE_MASK;
}
return ibus_input_context_process_key_event(_context, keyCode, keyCode, state);
}
private uint ConvertModifiers(KeyModifiers modifiers)
{
uint state = 0;
if (modifiers.HasFlag(KeyModifiers.Shift)) state |= IBUS_SHIFT_MASK;
if (modifiers.HasFlag(KeyModifiers.Control)) state |= IBUS_CONTROL_MASK;
if (modifiers.HasFlag(KeyModifiers.Alt)) state |= IBUS_MOD1_MASK;
if (modifiers.HasFlag(KeyModifiers.Super)) state |= IBUS_SUPER_MASK;
if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= IBUS_LOCK_MASK;
return state;
}
public void Reset()
{
if (_context != IntPtr.Zero)
{
ibus_input_context_reset(_context);
}
_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 (_context != IntPtr.Zero)
{
ibus_input_context_focus_out(_context);
g_object_unref(_context);
_context = IntPtr.Zero;
}
if (_bus != IntPtr.Zero)
{
g_object_unref(_bus);
_bus = IntPtr.Zero;
}
}
#region IBus Constants
private const uint IBUS_CAP_PREEDIT_TEXT = 1 << 0;
private const uint IBUS_CAP_FOCUS = 1 << 3;
private const uint IBUS_CAP_SURROUNDING_TEXT = 1 << 5;
private const uint IBUS_SHIFT_MASK = 1 << 0;
private const uint IBUS_LOCK_MASK = 1 << 1;
private const uint IBUS_CONTROL_MASK = 1 << 2;
private const uint IBUS_MOD1_MASK = 1 << 3;
private const uint IBUS_SUPER_MASK = 1 << 26;
private const uint IBUS_RELEASE_MASK = 1 << 30;
private const uint IBUS_ATTR_TYPE_UNDERLINE = 1;
private const uint IBUS_ATTR_TYPE_FOREGROUND = 2;
private const uint IBUS_ATTR_TYPE_BACKGROUND = 3;
#endregion
#region IBus Interop
private delegate void IBusCommitTextCallback(nint context, nint text, nint userData);
private delegate void IBusUpdatePreeditTextCallback(nint context, nint text, uint cursorPos, bool visible, nint userData);
private delegate void IBusShowPreeditTextCallback(nint context, nint userData);
private delegate void IBusHidePreeditTextCallback(nint context, nint userData);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_init();
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_bus_new();
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_bus_is_connected(nint bus);
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_bus_create_input_context(nint bus, string clientName);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_capabilities(nint context, uint capabilities);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_in(nint context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_out(nint context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_reset(nint context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_cursor_location(nint context, int x, int y, int w, int h);
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_input_context_process_key_event(nint context, uint keyval, uint keycode, uint state);
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_text_get_text(nint text);
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_text_get_attributes(nint text);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attr_list_size(nint attrList);
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_attr_list_get(nint attrList, uint index);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_attr_type(nint attr);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_start_index(nint attr);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_end_index(nint attr);
[DllImport("libgobject-2.0.so.0")]
private static extern void g_object_unref(nint obj);
[DllImport("libgobject-2.0.so.0")]
private static extern ulong g_signal_connect(nint instance, string signal, nint handler, nint data);
#endregion
}