diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings
index 25d62b0494..2c0a6b9dc8 100644
--- a/Avalonia.sln.DotSettings
+++ b/Avalonia.sln.DotSettings
@@ -38,4 +38,5 @@
<Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" />
<Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 1dc8c09c0e..ad705c66ea 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -109,7 +109,8 @@ namespace ControlCatalog.NetCore
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
- UseDBusMenu = true
+ UseDBusMenu = true,OverlayPopups = true,
+ EnableIme = true
})
.With(new Win32PlatformOptions
{
@@ -117,7 +118,7 @@ namespace ControlCatalog.NetCore
AllowEglInitialization = true
})
.UseSkia()
- .UseManagedSystemDialogs()
+ //.UseManagedSystemDialogs()
.LogToTrace();
static void SilenceConsole()
diff --git a/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
new file mode 100644
index 0000000000..61e2583a6c
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf differ
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index f001425964..423e4c77eb 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -11,6 +11,7 @@
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 4958174f40..c5226f3e58 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -5,6 +5,9 @@
+
+
diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
new file mode 100644
index 0000000000..9c29415a6a
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
@@ -0,0 +1,11 @@
+using Avalonia.Input;
+using Avalonia.Input.TextInput;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Platform
+{
+ public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl
+ {
+ public ITextInputMethodImpl TextInputMethod { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 078d8050bf..6bbb1c13bf 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -1,5 +1,6 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Threading;
@@ -378,19 +379,23 @@ namespace Avalonia.Controls.Presenters
if (_caretBlink)
{
- var charPos = FormattedText.HitTestTextPosition(CaretIndex);
- var x = Math.Floor(charPos.X) + 0.5;
- var y = Math.Floor(charPos.Y) + 0.5;
- var b = Math.Ceiling(charPos.Bottom) - 0.5;
-
+ var (p1, p2) = GetCaretPoints();
context.DrawLine(
new Pen(caretBrush, 1),
- new Point(x, y),
- new Point(x, b));
+ p1, p2);
}
}
}
+ (Point, Point) GetCaretPoints()
+ {
+ var charPos = FormattedText.HitTestTextPosition(CaretIndex);
+ var x = Math.Floor(charPos.X) + 0.5;
+ var y = Math.Floor(charPos.Y) + 0.5;
+ var b = Math.Ceiling(charPos.Bottom) - 0.5;
+ return (new Point(x, y), new Point(x, b));
+ }
+
public void ShowCaret()
{
_caretBlink = true;
@@ -538,5 +543,11 @@ namespace Avalonia.Controls.Presenters
_caretBlink = !_caretBlink;
InvalidateVisual();
}
+
+ internal Rect GetCursorRectangle()
+ {
+ var (p1, p2) = GetCaretPoints();
+ return new Rect(p1, p2);
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 0fe3ac62e4..e96efa7ce2 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -149,6 +149,7 @@ namespace Avalonia.Controls
private int _selectionStart;
private int _selectionEnd;
private TextPresenter _presenter;
+ private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
private UndoRedoHelper _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
@@ -161,6 +162,10 @@ namespace Avalonia.Controls
static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
+ TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) =>
+ {
+ e.Client = tb._imClient;
+ });
}
public TextBox()
@@ -437,7 +442,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get("PART_TextPresenter");
-
+ _imClient.SetPresenter(_presenter);
if (IsFocused)
{
_presenter?.ShowCaret();
diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
new file mode 100644
index 0000000000..ea664cecdc
--- /dev/null
+++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Controls.Presenters;
+using Avalonia.Input.TextInput;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ internal class TextBoxTextInputMethodClient : ITextInputMethodClient
+ {
+ private TextPresenter _presenter;
+ private IDisposable _subscription;
+ public Rect CursorRectangle => _presenter?.GetCursorRectangle() ?? default;
+ public event EventHandler CursorRectangleChanged;
+ public IVisual TextViewVisual => _presenter;
+ public event EventHandler TextViewVisualChanged;
+
+ private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+
+ public void SetPresenter(TextPresenter presenter)
+ {
+ _subscription?.Dispose();
+ _subscription = null;
+ _presenter = presenter;
+ if (_presenter != null)
+ {
+ _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty)
+ .Subscribe(OnCaretIndexChanged);
+ }
+ TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
+ CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 3d24f60463..4e43ce13b7 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -1,8 +1,10 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
@@ -31,6 +33,7 @@ namespace Avalonia.Controls
ICloseable,
IStyleHost,
ILogicalRoot,
+ ITextInputMethodRoot,
IWeakSubscriber
{
///
@@ -489,5 +492,8 @@ namespace Avalonia.Controls
if (focused == this)
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
}
+
+ ITextInputMethodImpl ITextInputMethodRoot.InputMethod =>
+ (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
new file mode 100644
index 0000000000..fbe9df55b6
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop
+{
+ class DBusCallQueue
+ {
+ class Item
+ {
+ public Func Callback;
+ public Func OnFinish;
+ }
+ private Queue- _q = new Queue
- ();
+ private bool _processing;
+
+ public void Enqueue(Func cb, Func onError)
+ {
+ _q.Enqueue(new Item
+ {
+ Callback = cb,
+ OnFinish = e =>
+ {
+ if (e != null)
+ return onError?.Invoke(e);
+ return Task.CompletedTask;
+ }
+ });
+ Process();
+ }
+
+ public Task EnqueueAsync(Func cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = cb,
+ OnFinish = e =>
+ {
+ if (e == null)
+ tcs.TrySetResult(0);
+ else
+ tcs.TrySetException(e);
+ return Task.CompletedTask;
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ public Task EnqueueAsync(Func> cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = async () =>
+ {
+ var res = await cb();
+ tcs.TrySetResult(res);
+ },
+ OnFinish = e =>
+ {
+ if (e != null)
+ tcs.TrySetException(e);
+ return Task.CompletedTask;
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ async void Process()
+ {
+ if(_processing)
+ return;
+ _processing = true;
+ try
+ {
+ while (_q.Count > 0)
+ {
+ var item = _q.Dequeue();
+ try
+ {
+ await item.Callback();
+ await item.OnFinish(null);
+ }
+ catch(Exception e)
+ {
+ await item.OnFinish(e);
+ }
+ }
+ }
+ finally
+ {
+ _processing = false;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index 91c4c28995..955d30626e 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
+using Avalonia.Logging;
using Avalonia.Threading;
using Tmds.DBus;
@@ -48,8 +49,10 @@ namespace Avalonia.FreeDesktop
}
public static Connection Connection { get; private set; }
- public static Exception TryInitialize(string dbusAddress = null)
+ public static Connection TryInitialize(string dbusAddress = null)
{
+ if (Connection != null)
+ return Connection;
var oldContext = SynchronizationContext.Current;
try
{
@@ -70,13 +73,15 @@ namespace Avalonia.FreeDesktop
}
catch (Exception e)
{
- return e;
+ Logger.TryGet(LogEventLevel.Error, "DBUS")
+ ?.Log(null, "Unable to connect to DBus: " + e);
}
finally
{
SynchronizationContext.SetSynchronizationContext(oldContext);
}
- return null;
+
+ return Connection;
}
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
new file mode 100644
index 0000000000..4e5113b1ee
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ [DBusInterface("org.fcitx.Fcitx.InputMethod")]
+ interface IFcitxInputMethod : IDBusObject
+ {
+ Task<(int icid, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICAsync();
+ Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv2Async(string Appname);
+ Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(string Appname, int Pid);
+ Task ExitAsync();
+ Task GetCurrentIMAsync();
+ Task SetCurrentIMAsync(string Im);
+ Task ReloadConfigAsync();
+ Task ReloadAddonConfigAsync(string Addon);
+ Task RestartAsync();
+ Task ConfigureAsync();
+ Task ConfigureAddonAsync(string Addon);
+ Task ConfigureIMAsync(string Im);
+ Task GetCurrentUIAsync();
+ Task GetIMAddonAsync(string Im);
+ Task ActivateIMAsync();
+ Task InactivateIMAsync();
+ Task ToggleIMAsync();
+ Task ResetIMListAsync();
+ Task GetCurrentStateAsync();
+ Task GetAsync(string prop);
+ Task GetAllAsync();
+ Task SetAsync(string prop, object val);
+ Task WatchPropertiesAsync(Action handler);
+ }
+
+ [Dictionary]
+ class FcitxInputMethodProperties
+ {
+ private (string, string, string, bool)[] _IMList = default((string, string, string, bool)[]);
+ public (string, string, string, bool)[] IMList
+ {
+ get
+ {
+ return _IMList;
+ }
+
+ set
+ {
+ _IMList = (value);
+ }
+ }
+
+ private string _CurrentIM = default(string);
+ public string CurrentIM
+ {
+ get
+ {
+ return _CurrentIM;
+ }
+
+ set
+ {
+ _CurrentIM = (value);
+ }
+ }
+ }
+
+ static class FcitxInputMethodExtensions
+ {
+ public static Task<(string, string, string, bool)[]> GetIMListAsync(this IFcitxInputMethod o) => o.GetAsync<(string, string, string, bool)[]>("IMList");
+ public static Task GetCurrentIMAsync(this IFcitxInputMethod o) => o.GetAsync("CurrentIM");
+ public static Task SetIMListAsync(this IFcitxInputMethod o, (string, string, string, bool)[] val) => o.SetAsync("IMList", val);
+ public static Task SetCurrentIMAsync(this IFcitxInputMethod o, string val) => o.SetAsync("CurrentIM", val);
+ }
+
+ [DBusInterface("org.fcitx.Fcitx.InputContext")]
+ interface IFcitxInputContext : IDBusObject
+ {
+ Task EnableICAsync();
+ Task CloseICAsync();
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task MouseEventAsync(int X);
+ Task SetCursorLocationAsync(int X, int Y);
+ Task SetCursorRectAsync(int X, int Y, int W, int H);
+ Task SetCapacityAsync(uint Caps);
+ Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+ Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+ Task DestroyICAsync();
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
+ Task WatchEnableIMAsync(Action handler, Action onError = null);
+ Task WatchCloseIMAsync(Action handler, Action onError = null);
+ Task WatchCommitStringAsync(Action handler, Action onError = null);
+ Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null);
+ Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null);
+ Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
new file mode 100644
index 0000000000..6510a5877a
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ enum FcitxKeyEventType
+ {
+ FCITX_PRESS_KEY,
+ FCITX_RELEASE_KEY
+ };
+
+ [Flags]
+ enum FcitxCapabilityFlags
+ {
+ CAPACITY_NONE = 0,
+ CAPACITY_CLIENT_SIDE_UI = (1 << 0),
+ CAPACITY_PREEDIT = (1 << 1),
+ CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2),
+ CAPACITY_PASSWORD = (1 << 3),
+ CAPACITY_FORMATTED_PREEDIT = (1 << 4),
+ CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5),
+ CAPACITY_SURROUNDING_TEXT = (1 << 6),
+ CAPACITY_EMAIL = (1 << 7),
+ CAPACITY_DIGIT = (1 << 8),
+ CAPACITY_UPPERCASE = (1 << 9),
+ CAPACITY_LOWERCASE = (1 << 10),
+ CAPACITY_NOAUTOUPPERCASE = (1 << 11),
+ CAPACITY_URL = (1 << 12),
+ CAPACITY_DIALABLE = (1 << 13),
+ CAPACITY_NUMBER = (1 << 14),
+ CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15),
+ CAPACITY_SPELLCHECK = (1 << 16),
+ CAPACITY_NO_SPELLCHECK = (1 << 17),
+ CAPACITY_WORD_COMPLETION = (1 << 18),
+ CAPACITY_UPPERCASE_WORDS = (1 << 19),
+ CAPACITY_UPPERCASE_SENTENCES = (1 << 20),
+ CAPACITY_ALPHA = (1 << 21),
+ CAPACITY_NAME = (1 << 22),
+ CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23),
+ CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24),
+ };
+
+ [Flags]
+ enum FcitxKeyState
+ {
+ FcitxKeyState_None = 0,
+ FcitxKeyState_Shift = 1 << 0,
+ FcitxKeyState_CapsLock = 1 << 1,
+ FcitxKeyState_Ctrl = 1 << 2,
+ FcitxKeyState_Alt = 1 << 3,
+ FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt,
+
+ FcitxKeyState_Ctrl_Alt_Shift =
+ FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_NumLock = 1 << 4,
+ FcitxKeyState_Super = 1 << 6,
+ FcitxKeyState_ScrollLock = 1 << 7,
+ FcitxKeyState_MousePressed = 1 << 8,
+ FcitxKeyState_HandledMask = 1 << 24,
+ FcitxKeyState_IgnoredMask = 1 << 25,
+ FcitxKeyState_Super2 = 1 << 26,
+ FcitxKeyState_Hyper = 1 << 27,
+ FcitxKeyState_Meta = 1 << 28,
+ FcitxKeyState_UsedMask = 0x5c001fff
+ };
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
new file mode 100644
index 0000000000..60f44e4651
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
@@ -0,0 +1,307 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Logging;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ internal class FcitxIx11TextInputMethodFactory : IX11InputMethodFactory
+ {
+ private readonly Connection _connection;
+
+ public FcitxIx11TextInputMethodFactory(Connection connection)
+ {
+ _connection = connection;
+ }
+
+ public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
+ {
+ var cl = new FcitxTextInputMethod(xid, _connection);
+ return (cl, cl);
+ }
+ }
+
+
+ internal class FcitxTextInputMethod : ITextInputMethodImpl, IX11InputMethodControl
+ {
+ private readonly IntPtr _xid;
+ private readonly Connection _connection;
+ private IFcitxInputContext _context;
+ private bool _connecting;
+ private string _currentName;
+ private DBusCallQueue _queue = new DBusCallQueue();
+ private bool _controlActive, _windowActive, _imeActive;
+ private Rect _logicalRect;
+ private double _scaling = 1;
+ private PixelPoint _windowPosition;
+ private bool _disposed;
+ private PixelRect? _lastReportedRect;
+ private FcitxCapabilityFlags _lastReportedFlags;
+
+ private List _disposables = new List();
+ private List _subscriptions = new List();
+ public FcitxTextInputMethod(IntPtr xid, Connection connection)
+ {
+ _xid = xid;
+ _connection = connection;
+ _disposables.Add(_connection.ResolveServiceOwnerAsync("org.fcitx.Fcitx", OnNameChange));
+ }
+
+ private async void OnNameChange(ServiceOwnerChangedEventArgs args)
+ {
+ if (args.NewOwner != null && _context == null && !_connecting)
+ {
+ _connecting = true;
+ try
+ {
+ var method = _connection.CreateProxy(args.ServiceName, "/inputmethod");
+ var resp = await method.CreateICv3Async(
+ Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia",
+ Process.GetCurrentProcess().Id);
+
+ _context = _connection.CreateProxy(args.ServiceName,
+ "/inputcontext_" + resp.icid);
+ _currentName = args.ServiceName;
+ _imeActive = false;
+ _lastReportedRect = null;
+ _lastReportedFlags = default;
+ _subscriptions.Add(await _context.WatchCommitStringAsync(OnCommitString));
+ _subscriptions.Add(await _context.WatchForwardKeyAsync(OnForward));
+ UpdateActive();
+ UpdateCursorRect();
+
+ }
+ catch(Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "FCITX")
+ ?.Log(this, "Unable to create fcitx input context:\n" + e);
+ }
+ finally
+ {
+ _connecting = false;
+ }
+
+ }
+
+ // fcitx has crashed
+ if (args.NewOwner == null && args.ServiceName == _currentName)
+ {
+ _context = null;
+ _currentName = null;
+ _imeActive = false;
+ foreach(var s in _subscriptions)
+ s.Dispose();
+ _subscriptions.Clear();
+ }
+ }
+
+ private void OnForward((uint keyval, uint state, int type) ev)
+ {
+ var state = (FcitxKeyState)ev.state;
+ KeyModifiers mods = default;
+ if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl))
+ mods |= KeyModifiers.Control;
+ if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt))
+ mods |= KeyModifiers.Alt;
+ if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift))
+ mods |= KeyModifiers.Shift;
+ if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super))
+ mods |= KeyModifiers.Meta;
+ _onForward?.Invoke(new X11InputMethodForwardedKey
+ {
+ Modifiers = mods,
+ KeyVal = (int)ev.keyval,
+ Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ?
+ RawKeyEventType.KeyDown :
+ RawKeyEventType.KeyUp
+ });
+ }
+
+ private void OnCommitString(string s) => _onCommit?.Invoke(s);
+
+ async Task OnError(Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "FCITX")
+ ?.Log(this, "Error:\n" + e);
+ try
+ {
+ await _context.DestroyICAsync();
+ }
+ catch (Exception ex)
+ {
+ Logger.TryGet(LogEventLevel.Error, "FCITX")
+ ?.Log(this, "Error while destroying the context:\n" + ex);
+ }
+
+ _context = null;
+ _currentName = null;
+ _imeActive = false;
+ }
+
+ void UpdateActive()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(_context == null)
+ return;
+
+ var active = _windowActive && _controlActive;
+ if (active != _imeActive)
+ {
+ _imeActive = active;
+ if (_imeActive)
+ await _context.FocusInAsync();
+ else
+ await _context.FocusOutAsync();
+ }
+ }, OnError);
+ }
+
+ void UpdateCursorRect()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(_context == null)
+ return;
+ var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
+ cursorRect = cursorRect.Translate(_windowPosition);
+ if (cursorRect != _lastReportedRect)
+ {
+ _lastReportedRect = cursorRect;
+ _context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
+ Math.Max(1, cursorRect.Height));
+ }
+ }, OnError);
+ }
+
+ public void SetOptions(TextInputOptionsQueryEventArgs options)
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(_context == null)
+ return;
+ FcitxCapabilityFlags flags = default;
+ if (options.Lowercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE;
+ if (options.Uppercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE;
+ if (!options.AutoCapitalization)
+ flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE;
+ if (options.ContentType == TextInputContentType.Email)
+ flags |= FcitxCapabilityFlags.CAPACITY_EMAIL;
+ else if (options.ContentType == TextInputContentType.Number)
+ flags |= FcitxCapabilityFlags.CAPACITY_NUMBER;
+ else if (options.ContentType == TextInputContentType.Password)
+ flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD;
+ else if (options.ContentType == TextInputContentType.Phone)
+ flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE;
+ else if (options.ContentType == TextInputContentType.Url)
+ flags |= FcitxCapabilityFlags.CAPACITY_URL;
+ if (flags != _lastReportedFlags)
+ {
+ _lastReportedFlags = flags;
+ await _context.SetCapacityAsync((uint)flags);
+ }
+ }, OnError);
+ }
+
+ public void SetActive(bool active)
+ {
+ _controlActive = active;
+ UpdateActive();
+ }
+
+ void IX11InputMethodControl.SetWindowActive(bool active)
+ {
+ _windowActive = active;
+ UpdateActive();
+ }
+
+ bool IX11InputMethodControl.IsEnabled => _context != null && _controlActive;
+
+ Task IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ return _queue.EnqueueAsync(async () =>
+ {
+ if (_context == null)
+ return false;
+ FcitxKeyState state = default;
+ if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
+ state |= FcitxKeyState.FcitxKeyState_Ctrl;
+ if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
+ state |= FcitxKeyState.FcitxKeyState_Alt;
+ if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
+ state |= FcitxKeyState.FcitxKeyState_Shift;
+ if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
+ state |= FcitxKeyState.FcitxKeyState_Super;
+
+ var type = args.Type == RawKeyEventType.KeyDown ?
+ FcitxKeyEventType.FCITX_PRESS_KEY :
+ FcitxKeyEventType.FCITX_RELEASE_KEY;
+
+ try
+ {
+ return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
+ (uint)args.Timestamp) != 0;
+ }
+ catch (Exception e)
+ {
+ await OnError(e);
+ return false;
+ }
+ });
+ }
+
+ private Action _onCommit;
+ event Action IX11InputMethodControl.OnCommit
+ {
+ add => _onCommit += value;
+ remove => _onCommit -= value;
+ }
+
+ private Action _onForward;
+ event Action IX11InputMethodControl.OnForwardKey
+ {
+ add => _onForward += value;
+ remove => _onForward -= value;
+ }
+
+ public void UpdateWindowInfo(PixelPoint position, double scaling)
+ {
+ _windowPosition = position;
+ _scaling = scaling;
+ UpdateCursorRect();
+ }
+
+ public void SetCursorRect(Rect rect)
+ {
+ _logicalRect = rect;
+ UpdateCursorRect();
+ }
+
+
+ void IDisposable.Dispose()
+ {
+ _disposed = true;
+ foreach(var d in _disposables)
+ d.Dispose();
+ _disposables.Clear();
+
+ foreach(var s in _subscriptions)
+ s.Dispose();
+ _subscriptions.Clear();
+
+ // fire and forget
+ _context?.DestroyICAsync().ContinueWith(_ => { });
+ _context = null;
+ _currentName = null;
+ }
+ }
+
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
new file mode 100644
index 0000000000..4ecfddf97b
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+ public class X11DBusImeHelper
+ {
+ private static readonly Dictionary> KnownMethods =
+ new Dictionary>
+ {
+ ["fcitx"] = conn => new FcitxIx11TextInputMethodFactory(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 != null && KnownMethods.TryGetValue(value, out var factory))
+ return factory;
+ }
+
+ return null;
+ }
+
+ public static void RegisterIfNeeded(bool? optionsWantIme)
+ {
+ if(
+ optionsWantIme == true
+ || Environment.GetEnvironmentVariable("AVALONIA_FORCE_IME") == "1"
+ || (optionsWantIme == null && IsCjkLocale(Environment.GetEnvironmentVariable("LANG"))))
+ {
+ var factory = DetectInputMethod();
+ if (factory != null)
+ {
+ var conn = DBusHelper.TryInitialize();
+ if (conn != null)
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs
new file mode 100644
index 0000000000..156e3611f4
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.FreeDesktop
+{
+ public interface IX11InputMethodFactory
+ {
+ (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid);
+ }
+
+ public struct X11InputMethodForwardedKey
+ {
+ public int KeyVal { get; set; }
+ public KeyModifiers Modifiers { get; set; }
+ public RawKeyEventType Type { get; set; }
+ }
+
+ public interface IX11InputMethodControl : IDisposable
+ {
+ void SetWindowActive(bool active);
+ bool IsEnabled { get; }
+ Task HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
+ event Action OnCommit;
+ event Action OnForwardKey;
+
+ void UpdateWindowInfo(PixelPoint position, double scaling);
+ }
+}
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 66fb9cfb1c..f3996cea76 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Data;
using Avalonia.Input.GestureRecognizers;
+using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -103,6 +104,22 @@ namespace Avalonia.Input
RoutedEvent.Register(
"TextInput",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextInputMethodClientRequestedEvent =
+ RoutedEvent.Register(
+ "TextInputMethodClientRequested",
+ RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextInputOptionsQueryEvent =
+ RoutedEvent.Register(
+ "TextInputOptionsQuery",
+ RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
///
/// Defines the event.
@@ -243,6 +260,24 @@ namespace Avalonia.Input
add { AddHandler(TextInputEvent, value); }
remove { RemoveHandler(TextInputEvent, value); }
}
+
+ ///
+ /// Occurs when an input element gains input focus and input method is looking for the corresponding client
+ ///
+ public event EventHandler TextInputMethodClientRequested
+ {
+ add { AddHandler(TextInputMethodClientRequestedEvent, value); }
+ remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); }
+ }
+
+ ///
+ /// Occurs when an input element gains input focus and input method is asking for required content options
+ ///
+ public event EventHandler TextInputOptionsQuery
+ {
+ add { AddHandler(TextInputOptionsQueryEvent, value); }
+ remove { RemoveHandler(TextInputOptionsQueryEvent, value); }
+ }
///
/// Occurs when the pointer enters the control.
diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs
index 6f4cb7a35c..5899824c29 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -18,6 +19,10 @@ namespace Avalonia.Input
public IInputManager InputManager => AvaloniaLocator.Current.GetService();
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService();
+
+ // This should live in the FocusManager, but with the current outdated architecture
+ // the source of truth about the input focus is in KeyboardDevice
+ private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager();
public IInputElement? FocusedElement
{
@@ -40,6 +45,7 @@ namespace Avalonia.Input
}
RaisePropertyChanged();
+ _textInputManager.SetFocusedElement(value);
}
}
diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
new file mode 100644
index 0000000000..227496cc4a
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
@@ -0,0 +1,13 @@
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input.TextInput
+{
+ public interface ITextInputMethodClient
+ {
+ Rect CursorRectangle { get; }
+ event EventHandler CursorRectangleChanged;
+ IVisual TextViewVisual { get; }
+ event EventHandler TextViewVisualChanged;
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
new file mode 100644
index 0000000000..d33bee9e8e
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Input.TextInput
+{
+ public interface ITextInputMethodImpl
+ {
+ void SetActive(bool active);
+ void SetCursorRect(Rect rect);
+ void SetOptions(TextInputOptionsQueryEventArgs options);
+ }
+
+ public interface ITextInputMethodRoot : IInputRoot
+ {
+ ITextInputMethodImpl InputMethod { get; }
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs
new file mode 100644
index 0000000000..592a12aec3
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/InputMethodManager.cs
@@ -0,0 +1,100 @@
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input.TextInput
+{
+ internal class TextInputMethodManager
+ {
+ private ITextInputMethodImpl? _im;
+ private IInputElement? _focusedElement;
+ private ITextInputMethodClient? _client;
+ private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
+
+ public TextInputMethodManager() => _transformTracker.MatrixChanged += UpdateCursorRect;
+
+ private ITextInputMethodClient? Client
+ {
+ get => _client;
+ set
+ {
+ if(_client == value)
+ return;
+ if (_client != null)
+ {
+ _client.CursorRectangleChanged -= OnCursorRectangleChanged;
+ _client.TextViewVisualChanged -= OnTextViewVisualChanged;
+ }
+
+ _client = value;
+
+ if (_client != null)
+ {
+ _client.CursorRectangleChanged += OnCursorRectangleChanged;
+ _client.TextViewVisualChanged += OnTextViewVisualChanged;
+ var optionsQuery = new TextInputOptionsQueryEventArgs
+ {
+ RoutedEvent = InputElement.TextInputOptionsQueryEvent
+ };
+ _focusedElement?.RaiseEvent(optionsQuery);
+ _im?.SetOptions(optionsQuery);
+ _transformTracker?.SetVisual(_client?.TextViewVisual);
+ UpdateCursorRect();
+ _im?.SetActive(true);
+ }
+ else
+ {
+ _im?.SetActive(false);
+ _transformTracker.SetVisual(null);
+ }
+ }
+ }
+
+ private void OnTextViewVisualChanged(object sender, EventArgs e)
+ => _transformTracker.SetVisual(_client?.TextViewVisual);
+
+ private void UpdateCursorRect()
+ {
+ if (_im == null || _client == null || _focusedElement?.VisualRoot == null)
+ return;
+ var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot);
+ if (transform == null)
+ _im.SetCursorRect(default);
+ else
+ _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value));
+ }
+
+ private void OnCursorRectangleChanged(object sender, EventArgs e)
+ {
+ if (sender == _client)
+ UpdateCursorRect();
+ }
+
+ public void SetFocusedElement(IInputElement? element)
+ {
+ if(_focusedElement == element)
+ return;
+ _focusedElement = element;
+
+ var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod;
+ if(_im != inputMethod)
+ _im?.SetActive(false);
+
+ _im = inputMethod;
+
+ if (_focusedElement == null || _im == null)
+ {
+ Client = null;
+ _im?.SetActive(false);
+ return;
+ }
+
+ var clientQuery = new TextInputMethodClientRequestedEventArgs
+ {
+ RoutedEvent = InputElement.TextInputMethodClientRequestedEvent
+ };
+
+ _focusedElement.RaiseEvent(clientQuery);
+ Client = clientQuery.Client;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs
new file mode 100644
index 0000000000..5d73fc1552
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/TextInputContentType.cs
@@ -0,0 +1,12 @@
+namespace Avalonia.Input.TextInput
+{
+ public enum TextInputContentType
+ {
+ Normal = 0,
+ Email = 1,
+ Phone = 2,
+ Number = 3,
+ Url = 4,
+ Password = 5
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
new file mode 100644
index 0000000000..bec43487d2
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
@@ -0,0 +1,12 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.TextInput
+{
+ public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs
+ {
+ ///
+ /// Set this property to a valid text input client to enable input method interaction
+ ///
+ public ITextInputMethodClient? Client { get; set; }
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
new file mode 100644
index 0000000000..924d0eb166
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
@@ -0,0 +1,32 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.TextInput
+{
+ public class TextInputOptionsQueryEventArgs : RoutedEventArgs
+ {
+ ///
+ /// The content type (mostly for determining the shape of the virtual keyboard)
+ ///
+ public TextInputContentType ContentType { get; set; }
+ ///
+ /// Text is multiline
+ ///
+ public bool Multiline { get; set; }
+ ///
+ /// Text is in lower case
+ ///
+ public bool Lowercase { get; set; }
+ ///
+ /// Text is in upper case
+ ///
+ public bool Uppercase { get; set; }
+ ///
+ /// Automatically capitalize letters at the start of the sentence
+ ///
+ public bool AutoCapitalization { get; set; }
+ ///
+ /// Text contains sensitive data like card numbers and should not be stored
+ ///
+ public bool IsSensitive { get; set; }
+ }
+}
diff --git a/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
new file mode 100644
index 0000000000..4211360a8f
--- /dev/null
+++ b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input.TextInput
+{
+ class TransformTrackingHelper : IDisposable
+ {
+ private IVisual? _visual;
+ private bool _queuedForUpdate;
+ private readonly EventHandler _propertyChangedHandler;
+ private readonly List _propertyChangedSubscriptions = new List();
+
+ public TransformTrackingHelper()
+ {
+ _propertyChangedHandler = PropertyChangedHandler;
+ }
+
+ public void SetVisual(IVisual? visual)
+ {
+ Dispose();
+ _visual = visual;
+ if (visual != null)
+ {
+ visual.AttachedToVisualTree += OnAttachedToVisualTree;
+ visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
+ if (visual.IsAttachedToVisualTree)
+ SubscribeToParents();
+ UpdateMatrix();
+ }
+ }
+
+ public Matrix? Matrix { get; private set; }
+ public event Action? MatrixChanged;
+
+ public void Dispose()
+ {
+ if(_visual == null)
+ return;
+ UnsubscribeFromParents();
+ _visual.AttachedToVisualTree -= OnAttachedToVisualTree;
+ _visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
+ _visual = null;
+ }
+
+ private void SubscribeToParents()
+ {
+ var visual = _visual;
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ // false positive
+ while (visual != null)
+ {
+ if (visual is Visual v)
+ {
+ v.PropertyChanged += _propertyChangedHandler;
+ _propertyChangedSubscriptions.Add(v);
+ }
+
+ visual = visual.VisualParent;
+ }
+ }
+
+ private void UnsubscribeFromParents()
+ {
+ foreach (var v in _propertyChangedSubscriptions)
+ v.PropertyChanged -= _propertyChangedHandler;
+ _propertyChangedSubscriptions.Clear();
+ }
+
+ void UpdateMatrix()
+ {
+ Matrix? matrix = null;
+ if (_visual != null && _visual.VisualRoot != null)
+ matrix = _visual.TransformToVisual(_visual.VisualRoot);
+ if (Matrix != matrix)
+ {
+ Matrix = matrix;
+ MatrixChanged?.Invoke();
+ }
+ }
+
+ private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs)
+ {
+ SubscribeToParents();
+ UpdateMatrix();
+ }
+
+ private void EnqueueForUpdate()
+ {
+ if(_queuedForUpdate)
+ return;
+ _queuedForUpdate = true;
+ Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.Render);
+ }
+
+ private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.IsEffectiveValueChange && e.Property == Visual.BoundsProperty)
+ EnqueueForUpdate();
+ }
+
+ private void OnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs)
+ {
+ UnsubscribeFromParents();
+ UpdateMatrix();
+ }
+ }
+}
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index c6db146f7b..e92fb259bd 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -4,6 +4,7 @@ using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop;
+using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
@@ -58,7 +59,7 @@ namespace Avalonia.X11
.Bind().ToConstant(new X11IconLoader(Info))
.Bind().ToConstant(new GtkSystemDialog())
.Bind().ToConstant(new LinuxMountedVolumeInfoProvider());
-
+ X11DBusImeHelper.RegisterIfNeeded(options.EnableIme);
X11Screens = Avalonia.X11.X11Screens.Init(this);
Screens = new X11Screens(X11Screens);
if (Info.XInputVersion != null)
@@ -103,6 +104,7 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true;
+ public bool? EnableIme { get; set; }
public IList GlProfiles { get; set; } = new List
{
diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs
new file mode 100644
index 0000000000..6ad790dff5
--- /dev/null
+++ b/src/Avalonia.X11/X11Window.Ime.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using static Avalonia.X11.XLib;
+
+namespace Avalonia.X11
+{
+ partial class X11Window
+ {
+ private ITextInputMethodImpl _ime;
+ private IX11InputMethodControl _imeControl;
+ private bool _processingIme;
+
+ private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue =
+ new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>();
+
+ void InitializeIme()
+ {
+ var ime = AvaloniaLocator.Current.GetService()?.CreateClient(_handle);
+ if (ime != null)
+ {
+ (_ime, _imeControl) = ime.Value;
+ _imeControl.OnCommit += s =>
+ ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
+ _inputRoot, s));
+ _imeControl.OnForwardKey += ev =>
+ {
+ ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
+ _inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal),
+ (RawInputModifiers)ev.Modifiers));
+ };
+ }
+ }
+
+ void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling);
+
+ async void HandleKeyEvent(XEvent ev)
+ {
+
+
+ var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
+
+ // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
+ var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
+
+ // Manually switch the Shift index for the keypad,
+ // there should be a proper way to do this
+ if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
+ && key > X11Key.Num_Lock && key <= X11Key.KP_9)
+ key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
+
+ var filtered = ScheduleKeyInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
+ ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
+ X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev, (int)key, ev.KeyEvent.keycode);
+
+ if (_handle == IntPtr.Zero)
+ return;
+
+ if (ev.type == XEventName.KeyPress && !filtered)
+ TriggerClassicTextInputEvent(ev);
+ }
+
+ void TriggerClassicTextInputEvent(XEvent ev)
+ {
+ var text = TranslateEventToString(ev);
+ if (text != null)
+ ScheduleInput(
+ new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
+ ref ev);
+ }
+
+ unsafe string TranslateEventToString(XEvent ev)
+ {
+ var buffer = stackalloc byte[40];
+ var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _);
+ if (len != 0)
+ {
+ var text = Encoding.UTF8.GetString(buffer, len);
+ if (text.Length == 1)
+ {
+ if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
+ return null;
+ }
+
+ return text;
+ }
+
+ return null;
+ }
+
+
+ bool ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode)
+ {
+ _x11.LastActivityTimestamp = xev.ButtonEvent.time;
+ if (_imeControl != null && _imeControl.IsEnabled)
+ {
+ if (FilterIme(args, xev, keyval, keycode))
+ return true;
+ }
+ ScheduleInput(args);
+ return false;
+ }
+
+ bool FilterIme(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)
+ {
+ if (_ime == null)
+ return false;
+ _imeQueue.Enqueue((args, xev, keyval, keycode));
+ if (!_processingIme)
+ ProcessNextImeEvent();
+
+ return true;
+ }
+
+ async void ProcessNextImeEvent()
+ {
+ if(_processingIme)
+ return;
+ _processingIme = true;
+ try
+ {
+ while (_imeQueue.Count != 0)
+ {
+ var ev = _imeQueue.Dequeue();
+ if (_imeControl == null || !await _imeControl.HandleEventAsync(ev.args, ev.keyval, ev.keycode))
+ {
+ ScheduleInput(ev.args);
+ if (ev.args.Type == RawKeyEventType.KeyDown)
+ TriggerClassicTextInputEvent(ev.xev);
+ }
+ }
+ }
+ finally
+ {
+ _processingIme = false;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 41c061613d..cb65dbe25f 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -5,12 +5,14 @@ using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
+using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.FreeDesktop;
using Avalonia.Input;
using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
@@ -22,9 +24,10 @@ using static Avalonia.X11.XLib;
// ReSharper disable StringLiteralTypo
namespace Avalonia.X11
{
- unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
+ unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
ITopLevelImplWithNativeMenuExporter,
- ITopLevelImplWithNativeControlHost
+ ITopLevelImplWithNativeControlHost,
+ ITopLevelImplWithTextInputMethod
{
private readonly AvaloniaX11Platform _platform;
private readonly IWindowImpl _popupParent;
@@ -178,11 +181,13 @@ namespace Avalonia.X11
Surfaces = surfaces.ToArray();
UpdateMotifHints();
UpdateSizeHints(null);
- _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
- XNames.XNClientWindow, _handle, IntPtr.Zero);
+
_transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
_transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
-
+ _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle,
+ XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
+ XNames.XNClientWindow, _handle, IntPtr.Zero);
+
XFlush(_x11.Display);
if(_popup)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
@@ -194,6 +199,7 @@ namespace Avalonia.X11
Paint?.Invoke(default);
return _handle != IntPtr.Zero;
}, TimeSpan.FromMilliseconds(100));
+ InitializeIme();
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@@ -386,9 +392,13 @@ namespace Avalonia.X11
if (ActivateTransientChildIfNeeded())
return;
Activated?.Invoke();
+ _imeControl.SetWindowActive(true);
}
else if (ev.type == XEventName.FocusOut)
+ {
+ _imeControl.SetWindowActive(false);
Deactivated?.Invoke();
+ }
else if (ev.type == XEventName.MotionNotify)
MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
else if (ev.type == XEventName.LeaveNotify)
@@ -477,6 +487,7 @@ namespace Avalonia.X11
PositionChanged?.Invoke(npos);
updatedSizeViaScaling = UpdateScaling();
}
+ UpdateImePosition();
if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize);
@@ -507,39 +518,7 @@ namespace Avalonia.X11
{
if (ActivateTransientChildIfNeeded())
return;
- var buffer = stackalloc byte[40];
-
- var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
-
- // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
- var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
-
- // Manually switch the Shift index for the keypad,
- // there should be a proper way to do this
- if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
- && key > X11Key.Num_Lock && key <= X11Key.KP_9)
- key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
-
-
- ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
- ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
- X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev);
-
- if (ev.type == XEventName.KeyPress)
- {
- var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _);
- if (len != 0)
- {
- var text = Encoding.UTF8.GetString(buffer, len);
- if (text.Length == 1)
- {
- if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
- return;
- }
- ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
- ref ev);
- }
- }
+ HandleKeyEvent(ev);
}
}
@@ -562,6 +541,7 @@ namespace Avalonia.X11
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
+ UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true);
@@ -699,6 +679,7 @@ namespace Avalonia.X11
_x11.LastActivityTimestamp = xev.ButtonEvent.time;
ScheduleInput(args);
}
+
public void ScheduleXI2Input(RawInputEventArgs args)
{
@@ -781,6 +762,13 @@ namespace Avalonia.X11
void Cleanup()
{
+ if (_imeControl != null)
+ {
+ _imeControl.Dispose();
+ _imeControl = null;
+ _ime = null;
+ }
+
if (_xic != IntPtr.Zero)
{
XDestroyIC(_xic);
@@ -1130,6 +1118,8 @@ namespace Avalonia.X11
public IPopupPositioner PopupPositioner { get; }
public ITopLevelNativeMenuExporter NativeMenuExporter { get; }
public INativeControlHostImpl NativeControlHost { get; }
+ public ITextInputMethodImpl TextInputMethod => _ime;
+
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
_transparencyHelper.SetTransparencyRequest(transparencyLevel);