diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 70c2c42bf1..a7e83140ae 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,6 +60,9 @@ namespace Avalonia.FreeDesktop.DBusIme } protected abstract Task Connect(string name); + + protected string GetAppName() => + Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; private async void OnNameChange(ServiceOwnerChangedEventArgs args) { diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index b649ee9119..8239b3f35d 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -25,11 +25,10 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx protected override async Task Connect(string name) { - var appName = Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; if (name == "org.fcitx.Fcitx") { var method = Connection.CreateProxy(name, "/inputmethod"); - var resp = await method.CreateICv3Async(appName, + var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); var proxy = Connection.CreateProxy(name, @@ -40,7 +39,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx else { var method = Connection.CreateProxy(name, "/inputmethod"); - var resp = await method.CreateInputContextAsync(new[] { ("appName", appName) }); + var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); var proxy = Connection.CreateProxy(name, resp.path); _context = new FcitxICWrapper(proxy); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs new file mode 100644 index 0000000000..26c0d249f3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [DBusInterface("org.freedesktop.IBus.InputContext")] + interface IIBusInputContext : IDBusObject + { + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); + Task SetCursorLocationAsync(int X, int Y, int W, int H); + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task SetCapabilitiesAsync(uint Caps); + Task PropertyActivateAsync(string Name, int State); + Task SetEngineAsync(string Name); + Task GetEngineAsync(); + Task DestroyAsync(); + Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); + Task WatchCommitTextAsync(Action cb, Action onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); + Task WatchShowLookupTableAsync(Action handler, Action onError = null); + Task WatchHideLookupTableAsync(Action handler, Action onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + } + + + [DBusInterface("org.freedesktop.IBus.Portal")] + interface IIBusPortal : IDBusObject + { + Task CreateInputContextAsync(string Name); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs new file mode 100644 index 0000000000..3070f51a8e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -0,0 +1,45 @@ +using System; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [Flags] + internal enum IBusModifierMask + { + ShiftMask = 1 << 0, + LockMask = 1 << 1, + ControlMask = 1 << 2, + Mod1Mask = 1 << 3, + Mod2Mask = 1 << 4, + Mod3Mask = 1 << 5, + Mod4Mask = 1 << 6, + Mod5Mask = 1 << 7, + Button1Mask = 1 << 8, + Button2Mask = 1 << 9, + Button3Mask = 1 << 10, + Button4Mask = 1 << 11, + Button5Mask = 1 << 12, + + HandledMask = 1 << 24, + ForwardMask = 1 << 25, + IgnoredMask = ForwardMask, + + SuperMask = 1 << 26, + HyperMask = 1 << 27, + MetaMask = 1 << 28, + + ReleaseMask = 1 << 30, + + ModifierMask = 0x5c001fff + } + + [Flags] + internal enum IBusCapability + { + CapPreeditText = 1 << 0, + CapAuxiliaryText = 1 << 1, + CapLookupTable = 1 << 2, + CapFocus = 1 << 3, + CapProperty = 1 << 4, + CapSurroundingText = 1 << 5, + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs new file mode 100644 index 0000000000..74f54267d0 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + internal class IBusX11TextInputMethod : DBusTextInputMethodBase + { + private IIBusInputContext _context; + + public IBusX11TextInputMethod(Connection connection) : base(connection, + "org.freedesktop.portal.IBus") + { + } + + protected override async Task Connect(string name) + { + var path = + await Connection.CreateProxy(name, "/org/freedesktop/IBus") + .CreateInputContextAsync(GetAppName()); + + _context = Connection.CreateProxy(name, path); + AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); + AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); + Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); + return true; + } + + private void OnForwardKey((uint keyval, uint keycode, uint state) k) + { + var state = (IBusModifierMask)k.state; + KeyModifiers mods = default; + if (state.HasFlagCustom(IBusModifierMask.ControlMask)) + mods |= KeyModifiers.Control; + if (state.HasFlagCustom(IBusModifierMask.Mod1Mask)) + mods |= KeyModifiers.Alt; + if (state.HasFlagCustom(IBusModifierMask.ShiftMask)) + mods |= KeyModifiers.Shift; + if (state.HasFlagCustom(IBusModifierMask.Mod4Mask)) + mods |= KeyModifiers.Meta; + FireForward(new X11InputMethodForwardedKey + { + KeyVal = (int)k.keyval, + Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown, + Modifiers = mods + }); + } + + + private void OnCommitText(object wtf) + { + // Hello darkness, my old friend + var prop = wtf.GetType().GetField("Item3"); + if (prop != null) + { + var text = (string)prop.GetValue(wtf); + if (!string.IsNullOrEmpty(text)) + FireCommit(text); + } + } + + protected override Task Disconnect() => _context.DestroyAsync(); + + protected override void OnDisconnected() + { + _context = null; + base.OnDisconnected(); + } + + protected override Task SetCursorRectCore(PixelRect rect) + => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + + protected override Task SetActiveCore(bool active) + => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + + protected override Task ResetContextCore() + => _context.ResetAsync(); + + protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) + { + IBusModifierMask state = default; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) + state |= IBusModifierMask.ControlMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) + state |= IBusModifierMask.Mod1Mask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) + state |= IBusModifierMask.ShiftMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) + state |= IBusModifierMask.Mod4Mask; + + if (args.Type == RawKeyEventType.KeyUp) + state |= IBusModifierMask.ReleaseMask; + + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + + public override void SetOptions(TextInputOptionsQueryEventArgs options) + { + // No-op, because ibus + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index fb01d67f3e..7f71ecf0ff 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.FreeDesktop.DBusIme.Fcitx; +using Avalonia.FreeDesktop.DBusIme.IBus; using Tmds.DBus; namespace Avalonia.FreeDesktop.DBusIme @@ -11,24 +12,20 @@ namespace Avalonia.FreeDesktop.DBusIme new Dictionary> { ["fcitx"] = conn => - new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)) + new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), + ["ibus"] = conn => + new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - static bool IsCjkLocale(string lang) - { - if (lang == null) - return false; - return lang.Contains("zh") - || lang.Contains("ja") - || lang.Contains("vi") - || lang.Contains("ko"); - } - static Func DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { var value = Environment.GetEnvironmentVariable(name); + + if (value == "none") + return null; + if (value != null && KnownMethods.TryGetValue(value, out var factory)) return factory; } @@ -36,22 +33,16 @@ namespace Avalonia.FreeDesktop.DBusIme return null; } - public static bool RegisterIfNeeded(bool? optionsWantIme) + public static bool DetectAndRegister() { - if( - optionsWantIme == true - || Environment.GetEnvironmentVariable("AVALONIA_FORCE_IME") == "1" - || (optionsWantIme == null && IsCjkLocale(Environment.GetEnvironmentVariable("LANG")))) + var factory = DetectInputMethod(); + if (factory != null) { - var factory = DetectInputMethod(); - if (factory != null) + var conn = DBusHelper.TryInitialize(); + if (conn != null) { - var conn = DBusHelper.TryInitialize(); - if (conn != null) - { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); - return true; - } + AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); + return true; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d52f5e1549..b871aa6fcf 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -39,9 +39,13 @@ namespace Avalonia.X11 Options = options; bool useXim = false; - if (!X11DBusImeHelper.RegisterIfNeeded(Options.EnableIme)) - useXim = ShouldUseXim(); - + if (EnableIme(options)) + { + // Attempt to configure DBus-based input method and check if we can fall back to XIM + if (!X11DBusImeHelper.DetectAndRegister() && ShouldUseXim()) + useXim = true; + } + // XIM doesn't work at all otherwise if (useXim) setlocale(0, ""); @@ -106,8 +110,36 @@ namespace Avalonia.X11 throw new NotSupportedException(); } + bool EnableIme(X11PlatformOptions options) + { + // Disable if explicitly asked by user + var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE"); + if (avaloniaImModule == "none") + return false; + + // Use value from options when specified + if (options.EnableIme.HasValue) + return options.EnableIme.Value; + + // Automatically enable for CJK locales + var lang = Environment.GetEnvironmentVariable("LANG"); + var isCjkLocale = lang != null && + (lang.Contains("zh") + || lang.Contains("ja") + || lang.Contains("vi") + || lang.Contains("ko")); + + return isCjkLocale; + } + bool ShouldUseXim() { + // Check if we are forbidden from using IME + if (Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "none" + || Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none" + || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none") + return true; + // Check if XIM is configured var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS"); if (modifiers == null) @@ -122,11 +154,6 @@ namespace Avalonia.X11 || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim" || Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim") return true; - - // Check if fallback is enabled - if (Options.ForceEnableXimFallback || - Environment.GetEnvironmentVariable("AVALONIA_FORCE_XIM_FALLBACK") == "1") - return true; return false; } @@ -144,7 +171,6 @@ namespace Avalonia public bool UseDBusMenu { get; set; } public bool UseDeferredRendering { get; set; } = true; public bool? EnableIme { get; set; } - public bool ForceEnableXimFallback { get; set; } public IList GlProfiles { get; set; } = new List {