Nikita Tsukanov 5 years ago
parent
commit
5ae9465ba1
  1. 3
      Avalonia.sln.DotSettings
  2. 5
      samples/ControlCatalog.NetCore/Program.cs
  3. BIN
      samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
  4. 1
      samples/ControlCatalog/MainView.xaml
  5. 6
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  6. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
  7. 25
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  8. 7
      src/Avalonia.Controls/TextBox.cs
  9. 33
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  10. 6
      src/Avalonia.Controls/TopLevel.cs
  11. 99
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  12. 11
      src/Avalonia.FreeDesktop/DBusHelper.cs
  13. 104
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  14. 67
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
  15. 307
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  16. 55
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  17. 31
      src/Avalonia.FreeDesktop/IX11InputMethod.cs
  18. 35
      src/Avalonia.Input/InputElement.cs
  19. 6
      src/Avalonia.Input/KeyboardDevice.cs
  20. 13
      src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
  21. 14
      src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
  22. 100
      src/Avalonia.Input/TextInput/InputMethodManager.cs
  23. 12
      src/Avalonia.Input/TextInput/TextInputContentType.cs
  24. 12
      src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  25. 32
      src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
  26. 109
      src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
  27. 4
      src/Avalonia.X11/X11Platform.cs
  28. 144
      src/Avalonia.X11/X11Window.Ime.cs
  29. 66
      src/Avalonia.X11/X11Window.cs

3
Avalonia.sln.DotSettings

@ -38,4 +38,5 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

5
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()

BIN
samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf

Binary file not shown.

1
samples/ControlCatalog/MainView.xaml

@ -11,6 +11,7 @@
</Style>
</Grid.Styles>
<TabControl Classes="sidebar" Name="Sidebar">
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="Acrylic"><pages:AcrylicPage/></TabItem>
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem>

6
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -5,6 +5,9 @@
<StackPanel Orientation="Vertical" Spacing="4">
<Label Classes="h1">TextBox</Label>
<Label Classes="h2">A control into which the user can input text</Label>
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="200" MaxWidth="400"
FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。&#xD;&#xD;有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
@ -64,5 +67,8 @@
<TextBox Width="200" Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
</StackPanel>
</StackPanel>
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="200" MaxWidth="400"
FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。&#xD;&#xD;有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
</StackPanel>
</UserControl>

11
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; }
}
}

25
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);
}
}
}

7
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<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
@ -161,6 +162,10 @@ namespace Avalonia.Controls
static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
TextInputMethodClientRequestedEvent.AddClassHandler<TextBox>((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<TextPresenter>("PART_TextPresenter");
_imClient.SetPresenter(_presenter);
if (IsFocused)
{
_presenter?.ShowCaret();

33
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);
}
}
}

6
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<ResourcesChangedEventArgs>
{
/// <summary>
@ -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;
}
}

99
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<Task> Callback;
public Func<Exception, Task> OnFinish;
}
private Queue<Item> _q = new Queue<Item>();
private bool _processing;
public void Enqueue(Func<Task> cb, Func<Exception, Task> onError)
{
_q.Enqueue(new Item
{
Callback = cb,
OnFinish = e =>
{
if (e != null)
return onError?.Invoke(e);
return Task.CompletedTask;
}
});
Process();
}
public Task EnqueueAsync(Func<Task> cb)
{
var tcs = new TaskCompletionSource<int>(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<T> EnqueueAsync<T>(Func<Task<T>> cb)
{
var tcs = new TaskCompletionSource<T>(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;
}
}
}
}

11
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;
}
}
}

104
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<string> GetCurrentIMAsync();
Task SetCurrentIMAsync(string Im);
Task ReloadConfigAsync();
Task ReloadAddonConfigAsync(string Addon);
Task RestartAsync();
Task ConfigureAsync();
Task ConfigureAddonAsync(string Addon);
Task ConfigureIMAsync(string Im);
Task<string> GetCurrentUIAsync();
Task<string> GetIMAddonAsync(string Im);
Task ActivateIMAsync();
Task InactivateIMAsync();
Task ToggleIMAsync();
Task ResetIMListAsync();
Task<int> GetCurrentStateAsync();
Task<T> GetAsync<T>(string prop);
Task<FcitxInputMethodProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> 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<string> GetCurrentIMAsync(this IFcitxInputMethod o) => o.GetAsync<string>("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<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
}
}

67
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
};
}

307
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<IDisposable> _disposables = new List<IDisposable>();
private List<IDisposable> _subscriptions = new List<IDisposable>();
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<IFcitxInputMethod>(args.ServiceName, "/inputmethod");
var resp = await method.CreateICv3Async(
Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia",
Process.GetCurrentProcess().Id);
_context = _connection.CreateProxy<IFcitxInputContext>(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<bool> IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
{
return _queue.EnqueueAsync<bool>(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<string> _onCommit;
event Action<string> IX11InputMethodControl.OnCommit
{
add => _onCommit += value;
remove => _onCommit -= value;
}
private Action<X11InputMethodForwardedKey> _onForward;
event Action<X11InputMethodForwardedKey> 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;
}
}
}

55
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<string, Func<Connection, IX11InputMethodFactory>> KnownMethods =
new Dictionary<string, Func<Connection, IX11InputMethodFactory>>
{
["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<Connection, IX11InputMethodFactory> 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<IX11InputMethodFactory>().ToConstant(factory(conn));
}
}
}
}
}

31
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<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
event Action<string> OnCommit;
event Action<X11InputMethodForwardedKey> OnForwardKey;
void UpdateWindowInfo(PixelPoint position, double scaling);
}
}

35
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<InputElement, TextInputEventArgs>(
"TextInput",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TextInputMethodClientRequested"/> event.
/// </summary>
public static readonly RoutedEvent<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequestedEvent =
RoutedEvent.Register<InputElement, TextInputMethodClientRequestedEventArgs>(
"TextInputMethodClientRequested",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TextInputOptionsQuery"/> event.
/// </summary>
public static readonly RoutedEvent<TextInputOptionsQueryEventArgs> TextInputOptionsQueryEvent =
RoutedEvent.Register<InputElement, TextInputOptionsQueryEventArgs>(
"TextInputOptionsQuery",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerEnter"/> event.
@ -243,6 +260,24 @@ namespace Avalonia.Input
add { AddHandler(TextInputEvent, value); }
remove { RemoveHandler(TextInputEvent, value); }
}
/// <summary>
/// Occurs when an input element gains input focus and input method is looking for the corresponding client
/// </summary>
public event EventHandler<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequested
{
add { AddHandler(TextInputMethodClientRequestedEvent, value); }
remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); }
}
/// <summary>
/// Occurs when an input element gains input focus and input method is asking for required content options
/// </summary>
public event EventHandler<TextInputOptionsQueryEventArgs> TextInputOptionsQuery
{
add { AddHandler(TextInputOptionsQueryEvent, value); }
remove { RemoveHandler(TextInputOptionsQueryEvent, value); }
}
/// <summary>
/// Occurs when the pointer enters the control.

6
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<IInputManager>();
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
// 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);
}
}

13
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;
}
}

14
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; }
}
}

100
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;
}
}
}

12
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
}
}

12
src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs

@ -0,0 +1,12 @@
using Avalonia.Interactivity;
namespace Avalonia.Input.TextInput
{
public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs
{
/// <summary>
/// Set this property to a valid text input client to enable input method interaction
/// </summary>
public ITextInputMethodClient? Client { get; set; }
}
}

32
src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs

@ -0,0 +1,32 @@
using Avalonia.Interactivity;
namespace Avalonia.Input.TextInput
{
public class TextInputOptionsQueryEventArgs : RoutedEventArgs
{
/// <summary>
/// The content type (mostly for determining the shape of the virtual keyboard)
/// </summary>
public TextInputContentType ContentType { get; set; }
/// <summary>
/// Text is multiline
/// </summary>
public bool Multiline { get; set; }
/// <summary>
/// Text is in lower case
/// </summary>
public bool Lowercase { get; set; }
/// <summary>
/// Text is in upper case
/// </summary>
public bool Uppercase { get; set; }
/// <summary>
/// Automatically capitalize letters at the start of the sentence
/// </summary>
public bool AutoCapitalization { get; set; }
/// <summary>
/// Text contains sensitive data like card numbers and should not be stored
/// </summary>
public bool IsSensitive { get; set; }
}
}

109
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<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
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();
}
}
}

4
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<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog())
.Bind<IMountedVolumeInfoProvider>().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<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{

144
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<IX11InputMethodFactory>()?.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;
}
}
}
}

66
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);

Loading…
Cancel
Save