Browse Source

Merge pull request #5258 from AvaloniaUI/linux-ime-cjk

Implemented basic IME support for Linux
pull/5322/head
Dan Walmsley 5 years ago
committed by GitHub
parent
commit
3618bccd8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Avalonia.sln.DotSettings
  2. 3
      samples/ControlCatalog.NetCore/Program.cs
  3. BIN
      samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
  4. 3
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  5. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
  6. 25
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  7. 7
      src/Avalonia.Controls/TextBox.cs
  8. 41
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  9. 6
      src/Avalonia.Controls/TopLevel.cs
  10. 110
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  11. 11
      src/Avalonia.FreeDesktop/DBusHelper.cs
  12. 288
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  13. 69
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  14. 67
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
  15. 51
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  16. 149
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  17. 52
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  18. 45
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
  19. 105
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  20. 53
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  21. 31
      src/Avalonia.FreeDesktop/IX11InputMethod.cs
  22. 35
      src/Avalonia.Input/InputElement.cs
  23. 6
      src/Avalonia.Input/KeyboardDevice.cs
  24. 60
      src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
  25. 15
      src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
  26. 101
      src/Avalonia.Input/TextInput/InputMethodManager.cs
  27. 12
      src/Avalonia.Input/TextInput/TextInputContentType.cs
  28. 12
      src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  29. 32
      src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
  30. 109
      src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
  31. 4
      src/Avalonia.X11/X11Clipboard.cs
  32. 4
      src/Avalonia.X11/X11Globals.cs
  33. 25
      src/Avalonia.X11/X11Info.cs
  34. 72
      src/Avalonia.X11/X11Platform.cs
  35. 11
      src/Avalonia.X11/X11PlatformThreading.cs
  36. 2
      src/Avalonia.X11/X11Screens.cs
  37. 130
      src/Avalonia.X11/X11Structs.cs
  38. 208
      src/Avalonia.X11/X11Window.Ime.cs
  39. 121
      src/Avalonia.X11/X11Window.Xim.cs
  40. 80
      src/Avalonia.X11/X11Window.cs
  41. 66
      src/Avalonia.X11/XLib.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>

3
samples/ControlCatalog.NetCore/Program.cs

@ -109,7 +109,8 @@ namespace ControlCatalog.NetCore
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
UseDBusMenu = true
UseDBusMenu = true,
EnableIme = true,
})
.With(new Win32PlatformOptions
{

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

Binary file not shown.

3
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -64,5 +64,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();

41
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -0,0 +1,41 @@
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;
public bool SupportsPreedit => false;
public void SetPreeditText(string text) => throw new NotSupportedException();
public bool SupportsSurroundingText => false;
public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException();
public event EventHandler SurroundingTextChanged;
public string TextBeforeCursor => null;
public string TextAfterCursor => null;
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;
}
}

110
src/Avalonia.FreeDesktop/DBusCallQueue.cs

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Avalonia.FreeDesktop
{
class DBusCallQueue
{
private readonly Func<Exception, Task> _errorHandler;
class Item
{
public Func<Task> Callback;
public Action<Exception> OnFinish;
}
private Queue<Item> _q = new Queue<Item>();
private bool _processing;
public DBusCallQueue(Func<Exception, Task> errorHandler)
{
_errorHandler = errorHandler;
}
public void Enqueue(Func<Task> cb)
{
_q.Enqueue(new Item
{
Callback = cb
});
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);
}
});
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);
}
});
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();
item.OnFinish?.Invoke(null);
}
catch(Exception e)
{
if (item.OnFinish != null)
item.OnFinish(e);
else
await _errorHandler(e);
}
}
}
finally
{
_processing = false;
}
}
public void FailAll()
{
while (_q.Count>0)
{
var item = _q.Dequeue();
item.OnFinish?.Invoke(new OperationCanceledException());
}
}
}
}

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

288
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.FreeDesktop.DBusIme.Fcitx;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Logging;
using Tmds.DBus;
namespace Avalonia.FreeDesktop.DBusIme
{
internal class DBusInputMethodFactory<T> : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
{
private readonly Func<IntPtr, T> _factory;
public DBusInputMethodFactory(Func<IntPtr, T> factory)
{
_factory = factory;
}
public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
{
var im = _factory(xid);
return (im, im);
}
}
internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
{
private List<IDisposable> _disposables = new List<IDisposable>();
private Queue<string> _onlineNamesQueue = new Queue<string>();
protected Connection Connection { get; }
private readonly string[] _knownNames;
private bool _connecting;
private string _currentName;
private DBusCallQueue _queue;
private bool _controlActive, _windowActive;
private bool? _imeActive;
private Rect _logicalRect;
private PixelRect? _lastReportedRect;
private double _scaling = 1;
private PixelPoint _windowPosition;
protected bool IsConnected => _currentName != null;
public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
{
_queue = new DBusCallQueue(QueueOnError);
Connection = connection;
_knownNames = knownNames;
Watch();
}
async void Watch()
{
foreach (var name in _knownNames)
_disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
}
protected abstract Task<bool> Connect(string name);
protected string GetAppName() =>
Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
private async void OnNameChange(ServiceOwnerChangedEventArgs args)
{
if (args.NewOwner != null && _currentName == null)
{
_onlineNamesQueue.Enqueue(args.ServiceName);
if(!_connecting)
{
_connecting = true;
try
{
while (_onlineNamesQueue.Count > 0)
{
var name = _onlineNamesQueue.Dequeue();
try
{
if (await Connect(name))
{
_onlineNamesQueue.Clear();
_currentName = name;
return;
}
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Unable to create IME input context:\n" + e);
}
}
}
finally
{
_connecting = false;
}
}
}
// IME has crashed
if (args.NewOwner == null && args.ServiceName == _currentName)
{
_currentName = null;
foreach(var s in _disposables)
s.Dispose();
_disposables.Clear();
OnDisconnected();
Reset();
// Watch again
Watch();
}
}
protected virtual Task Disconnect()
{
return Task.CompletedTask;
}
protected virtual void OnDisconnected()
{
}
protected virtual void Reset()
{
_lastReportedRect = null;
_imeActive = null;
}
async Task QueueOnError(Exception e)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Error:\n" + e);
try
{
await Disconnect();
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Error while destroying the context:\n" + ex);
}
OnDisconnected();
_currentName = null;
}
protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb);
protected void AddDisposable(IDisposable d) => _disposables.Add(d);
public void Dispose()
{
foreach(var d in _disposables)
d.Dispose();
_disposables.Clear();
try
{
Disconnect().ContinueWith(_ => { });
}
catch
{
// fire and forget
}
_currentName = null;
}
protected abstract Task SetCursorRectCore(PixelRect rect);
protected abstract Task SetActiveCore(bool active);
protected abstract Task ResetContextCore();
protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
void UpdateActive()
{
_queue.Enqueue(async () =>
{
if(!IsConnected)
return;
var active = _windowActive && _controlActive;
if (active != _imeActive)
{
_imeActive = active;
await SetActiveCore(active);
}
});
}
void IX11InputMethodControl.SetWindowActive(bool active)
{
_windowActive = active;
UpdateActive();
}
void ITextInputMethodImpl.SetActive(bool active)
{
_controlActive = active;
UpdateActive();
}
bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
async ValueTask<bool> IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
{
try
{
return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode));
}
// Disconnected
catch (OperationCanceledException)
{
return false;
}
// Error, disconnect
catch (Exception e)
{
await QueueOnError(e);
return false;
}
}
private Action<string> _onCommit;
event Action<string> IX11InputMethodControl.Commit
{
add => _onCommit += value;
remove => _onCommit -= value;
}
protected void FireCommit(string s) => _onCommit?.Invoke(s);
private Action<X11InputMethodForwardedKey> _onForward;
event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
{
add => _onForward += value;
remove => _onForward -= value;
}
protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
void UpdateCursorRect()
{
_queue.Enqueue(async () =>
{
if(!IsConnected)
return;
var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
cursorRect = cursorRect.Translate(_windowPosition);
if (cursorRect != _lastReportedRect)
{
_lastReportedRect = cursorRect;
await SetCursorRectCore(cursorRect);
}
});
}
void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
{
_windowPosition = position;
_scaling = scaling;
UpdateCursorRect();
}
void ITextInputMethodImpl.SetCursorRect(Rect rect)
{
_logicalRect = rect;
UpdateCursorRect();
}
public abstract void SetOptions(TextInputOptionsQueryEventArgs options);
void ITextInputMethodImpl.Reset()
{
Reset();
_queue.Enqueue(async () =>
{
if (!IsConnected)
return;
await ResetContextCore();
});
}
}
}

69
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@ -0,0 +1,69 @@
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, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(
string Appname, int Pid);
}
[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);
}
[DBusInterface("org.fcitx.Fcitx.InputContext1")]
interface IFcitxInputContext1 : IDBusObject
{
Task FocusInAsync();
Task FocusOutAsync();
Task ResetAsync();
Task SetCursorRectAsync(int X, int Y, int W, int H);
Task SetCapabilityAsync(ulong Caps);
Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync();
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
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> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputMethod1")]
interface IFcitxInputMethod1 : IDBusObject
{
Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
}
}

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

51
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxICWrapper
{
private readonly IFcitxInputContext1 _modern;
private readonly IFcitxInputContext _old;
public FcitxICWrapper(IFcitxInputContext old)
{
_old = old;
}
public FcitxICWrapper(IFcitxInputContext1 modern)
{
_modern = modern;
}
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync();
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync();
public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync();
public Task SetCursorRectAsync(int x, int y, int w, int h) =>
_old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h);
public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync();
public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
{
if(_old!=null)
return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time);
}
public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
public Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
{
return _old?.WatchForwardKeyAsync(handler)
?? _modern.WatchForwardKeyAsync(ev =>
handler((ev.keyval, ev.state, ev.type ? 1 : 0)));
}
public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags);
}
}

149
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@ -0,0 +1,149 @@
using System;
using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Tmds.DBus;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
{
private FcitxICWrapper _context;
private FcitxCapabilityFlags? _lastReportedFlags;
public FcitxX11TextInputMethod(Connection connection) : base(connection,
"org.fcitx.Fcitx",
"org.freedesktop.portal.Fcitx"
)
{
}
protected override async Task<bool> Connect(string name)
{
if (name == "org.fcitx.Fcitx")
{
var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod");
var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id);
var proxy = Connection.CreateProxy<IFcitxInputContext>(name,
"/inputcontext_" + resp.icid);
_context = new FcitxICWrapper(proxy);
}
else
{
var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod");
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
var proxy = Connection.CreateProxy<IFcitxInputContext1>(name, resp.path);
_context = new FcitxICWrapper(proxy);
}
AddDisposable(await _context.WatchCommitStringAsync(OnCommitString));
AddDisposable(await _context.WatchForwardKeyAsync(OnForward));
return true;
}
protected override Task Disconnect() => _context.DestroyICAsync();
protected override void OnDisconnected() => _context = null;
protected override void Reset()
{
_lastReportedFlags = null;
base.Reset();
}
protected override Task SetCursorRectCore(PixelRect cursorRect) =>
_context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
Math.Max(1, cursorRect.Height));
protected override Task SetActiveCore(bool active)
{
if (active)
return _context.FocusInAsync();
else
return _context.FocusOutAsync();
}
protected override Task ResetContextCore() => _context.ResetAsync();
protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{
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;
return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
(uint)args.Timestamp).ConfigureAwait(false);
}
public override void SetOptions(TextInputOptionsQueryEventArgs options) =>
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);
}
});
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;
FireForward(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) => FireCommit(s);
}
}

52
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
[DBusInterface("org.freedesktop.IBus.InputContext")]
interface IIBusInputContext : IDBusObject
{
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State);
Task SetCursorLocationAsync(int X, int Y, int W, int H);
Task FocusInAsync();
Task FocusOutAsync();
Task ResetAsync();
Task SetCapabilitiesAsync(uint Caps);
Task PropertyActivateAsync(string Name, int State);
Task SetEngineAsync(string Name);
Task<object> GetEngineAsync();
Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null);
}
[DBusInterface("org.freedesktop.IBus.Portal")]
interface IIBusPortal : IDBusObject
{
Task<ObjectPath> CreateInputContextAsync(string Name);
}
}

45
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs

@ -0,0 +1,45 @@
using System;
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
[Flags]
internal enum IBusModifierMask
{
ShiftMask = 1 << 0,
LockMask = 1 << 1,
ControlMask = 1 << 2,
Mod1Mask = 1 << 3,
Mod2Mask = 1 << 4,
Mod3Mask = 1 << 5,
Mod4Mask = 1 << 6,
Mod5Mask = 1 << 7,
Button1Mask = 1 << 8,
Button2Mask = 1 << 9,
Button3Mask = 1 << 10,
Button4Mask = 1 << 11,
Button5Mask = 1 << 12,
HandledMask = 1 << 24,
ForwardMask = 1 << 25,
IgnoredMask = ForwardMask,
SuperMask = 1 << 26,
HyperMask = 1 << 27,
MetaMask = 1 << 28,
ReleaseMask = 1 << 30,
ModifierMask = 0x5c001fff
}
[Flags]
internal enum IBusCapability
{
CapPreeditText = 1 << 0,
CapAuxiliaryText = 1 << 1,
CapLookupTable = 1 << 2,
CapFocus = 1 << 3,
CapProperty = 1 << 4,
CapSurroundingText = 1 << 5,
}
}

105
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Tmds.DBus;
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
internal class IBusX11TextInputMethod : DBusTextInputMethodBase
{
private IIBusInputContext _context;
public IBusX11TextInputMethod(Connection connection) : base(connection,
"org.freedesktop.portal.IBus")
{
}
protected override async Task<bool> Connect(string name)
{
var path =
await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus")
.CreateInputContextAsync(GetAppName());
_context = Connection.CreateProxy<IIBusInputContext>(name, path);
AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus));
return true;
}
private void OnForwardKey((uint keyval, uint keycode, uint state) k)
{
var state = (IBusModifierMask)k.state;
KeyModifiers mods = default;
if (state.HasFlagCustom(IBusModifierMask.ControlMask))
mods |= KeyModifiers.Control;
if (state.HasFlagCustom(IBusModifierMask.Mod1Mask))
mods |= KeyModifiers.Alt;
if (state.HasFlagCustom(IBusModifierMask.ShiftMask))
mods |= KeyModifiers.Shift;
if (state.HasFlagCustom(IBusModifierMask.Mod4Mask))
mods |= KeyModifiers.Meta;
FireForward(new X11InputMethodForwardedKey
{
KeyVal = (int)k.keyval,
Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
Modifiers = mods
});
}
private void OnCommitText(object wtf)
{
// Hello darkness, my old friend
var prop = wtf.GetType().GetField("Item3");
if (prop != null)
{
var text = (string)prop.GetValue(wtf);
if (!string.IsNullOrEmpty(text))
FireCommit(text);
}
}
protected override Task Disconnect() => _context.DestroyAsync();
protected override void OnDisconnected()
{
_context = null;
base.OnDisconnected();
}
protected override Task SetCursorRectCore(PixelRect rect)
=> _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height);
protected override Task SetActiveCore(bool active)
=> active ? _context.FocusInAsync() : _context.FocusOutAsync();
protected override Task ResetContextCore()
=> _context.ResetAsync();
protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{
IBusModifierMask state = default;
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
state |= IBusModifierMask.ControlMask;
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
state |= IBusModifierMask.Mod1Mask;
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
state |= IBusModifierMask.ShiftMask;
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
state |= IBusModifierMask.Mod4Mask;
if (args.Type == RawKeyEventType.KeyUp)
state |= IBusModifierMask.ReleaseMask;
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
}
public override void SetOptions(TextInputOptionsQueryEventArgs options)
{
// No-op, because ibus
}
}
}

53
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Avalonia.FreeDesktop.DBusIme.Fcitx;
using Avalonia.FreeDesktop.DBusIme.IBus;
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 DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
["ibus"] = conn =>
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
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 == "none")
return null;
if (value != null && KnownMethods.TryGetValue(value, out var factory))
return factory;
}
return null;
}
public static bool DetectAndRegister()
{
var factory = DetectInputMethod();
if (factory != null)
{
var conn = DBusHelper.TryInitialize();
if (conn != null)
{
AvaloniaLocator.CurrentMutable.Bind<IX11InputMethodFactory>().ToConstant(factory(conn));
return true;
}
}
return false;
}
}
}

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; }
ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
event Action<string> Commit;
event Action<X11InputMethodForwardedKey> ForwardKey;
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);
}
}

60
src/Avalonia.Input/TextInput/ITextInputMethodClient.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
{
public interface ITextInputMethodClient
{
/// <summary>
/// The cursor rectangle relative to the TextViewVisual
/// </summary>
Rect CursorRectangle { get; }
/// <summary>
/// Should be fired when cursor rectangle is changed inside the TextViewVisual
/// </summary>
event EventHandler CursorRectangleChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
IVisual TextViewVisual { get; }
/// <summary>
/// Should be fired when text-hosting visual is changed
/// </summary>
event EventHandler TextViewVisualChanged;
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position
/// </summary>
bool SupportsPreedit { get; }
/// <summary>
/// Sets the non-commited input string
/// </summary>
void SetPreeditText(string text);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position
/// </summary>
TextInputMethodSurroundingText SurroundingText { get; }
/// <summary>
/// Should be fired when surrounding text changed
/// </summary>
event EventHandler SurroundingTextChanged;
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
/// </summary>
string TextBeforeCursor { get; }
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
/// </summary>
string TextAfterCursor { get; }
}
public struct TextInputMethodSurroundingText
{
public string Text { get; set; }
public int CursorOffset { get; set; }
public int AnchorOffset { get; set; }
}
}

15
src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs

@ -0,0 +1,15 @@
namespace Avalonia.Input.TextInput
{
public interface ITextInputMethodImpl
{
void SetActive(bool active);
void SetCursorRect(Rect rect);
void SetOptions(TextInputOptionsQueryEventArgs options);
void Reset();
}
public interface ITextInputMethodRoot : IInputRoot
{
ITextInputMethodImpl InputMethod { get; }
}
}

101
src/Avalonia.Input/TextInput/InputMethodManager.cs

@ -0,0 +1,101 @@
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?.Reset();
_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/X11Clipboard.cs

@ -52,7 +52,7 @@ namespace Avalonia.X11
: null;
}
private unsafe void OnEvent(XEvent ev)
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionRequest)
{
@ -62,7 +62,7 @@ namespace Avalonia.X11
SelectionEvent =
{
type = XEventName.SelectionNotify,
send_event = true,
send_event = 1,
display = _x11.Display,
selection = sel.selection,
target = sel.target,

4
src/Avalonia.X11/X11Globals.cs

@ -123,7 +123,7 @@ namespace Avalonia.X11
}
}
private void HandleCompositionAtomOwnerEvents(XEvent ev)
private void HandleCompositionAtomOwnerEvents(ref XEvent ev)
{
if(ev.type == XEventName.DestroyNotify)
UpdateCompositingAtomOwner();
@ -154,7 +154,7 @@ namespace Avalonia.X11
}
}
private void OnRootWindowEvent(XEvent ev)
private void OnRootWindowEvent(ref XEvent ev)
{
if (ev.type == XEventName.PropertyNotify)
{

25
src/Avalonia.X11/X11Info.cs

@ -32,8 +32,10 @@ namespace Avalonia.X11
public IntPtr LastActivityTimestamp { get; set; }
public XVisualInfo? TransparentVisualInfo { get; set; }
public bool HasXim { get; set; }
public IntPtr DefaultFontSet { get; set; }
public unsafe X11Info(IntPtr display, IntPtr deferredDisplay)
public unsafe X11Info(IntPtr display, IntPtr deferredDisplay, bool useXim)
{
Display = display;
DeferredDisplay = deferredDisplay;
@ -43,9 +45,24 @@ namespace Avalonia.X11
DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow);
DefaultRootWindow = XDefaultRootWindow(display);
Atoms = new X11Atoms(display);
//TODO: Open an actual XIM once we get support for preedit in our textbox
XSetLocaleModifiers("@im=none");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
DefaultFontSet = XCreateFontSet(Display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*",
out var _, out var _, IntPtr.Zero);
if (useXim)
{
XSetLocaleModifiers("");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (Xim != IntPtr.Zero)
HasXim = true;
}
if (Xim == IntPtr.Zero)
{
XSetLocaleModifiers("@im=none");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
XMatchVisualInfo(Display, DefaultScreen, 32, 4, out var visual);
if (visual.depth == 32)
TransparentVisualInfo = visual;

72
src/Avalonia.X11/X11Platform.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop;
using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
@ -21,7 +23,8 @@ namespace Avalonia.X11
{
private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows =
new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
public XI2Manager XI2;
public X11Info Info { get; private set; }
public IX11Screens X11Screens { get; private set; }
@ -29,9 +32,24 @@ namespace Avalonia.X11
public X11PlatformOptions Options { get; private set; }
public IntPtr OrphanedWindow { get; private set; }
public X11Globals Globals { get; private set; }
[DllImport("libc")]
static extern void setlocale(int type, string s);
public void Initialize(X11PlatformOptions options)
{
Options = options;
bool useXim = false;
if (EnableIme(options))
{
// Attempt to configure DBus-based input method and check if we can fall back to XIM
if (!X11DBusImeHelper.DetectAndRegister() && ShouldUseXim())
useXim = true;
}
// XIM doesn't work at all otherwise
if (useXim)
setlocale(0, "");
XInitThreads();
Display = XOpenDisplay(IntPtr.Zero);
DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@ -40,7 +58,8 @@ namespace Avalonia.X11
if (Display == IntPtr.Zero)
throw new Exception("XOpenDisplay failed");
XError.Init();
Info = new X11Info(Display, DeferredDisplay);
Info = new X11Info(Display, DeferredDisplay, useXim);
Globals = new X11Globals(this);
//TODO: log
if (options.UseDBusMenu)
@ -90,6 +109,54 @@ namespace Avalonia.X11
{
throw new NotSupportedException();
}
bool EnableIme(X11PlatformOptions options)
{
// Disable if explicitly asked by user
var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE");
if (avaloniaImModule == "none")
return false;
// Use value from options when specified
if (options.EnableIme.HasValue)
return options.EnableIme.Value;
// Automatically enable for CJK locales
var lang = Environment.GetEnvironmentVariable("LANG");
var isCjkLocale = lang != null &&
(lang.Contains("zh")
|| lang.Contains("ja")
|| lang.Contains("vi")
|| lang.Contains("ko"));
return isCjkLocale;
}
bool ShouldUseXim()
{
// Check if we are forbidden from using IME
if (Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "none"
|| Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none"
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none")
return true;
// Check if XIM is configured
var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS");
if (modifiers == null)
return false;
if (modifiers.Contains("@im=none") || modifiers.Contains("@im=null"))
return false;
if (!modifiers.Contains("@im="))
return false;
// Check if we are configured to use it
if (Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "xim"
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim"
|| Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim")
return true;
return false;
}
}
}
@ -103,6 +170,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>
{

11
src/Avalonia.X11/X11PlatformThreading.cs

@ -13,7 +13,9 @@ namespace Avalonia.X11
{
private readonly AvaloniaX11Platform _platform;
private readonly IntPtr _display;
private readonly Dictionary<IntPtr, Action<XEvent>> _eventHandlers;
public delegate void EventHandler(ref XEvent xev);
private readonly Dictionary<IntPtr, EventHandler> _eventHandlers;
private Thread _mainThread;
[StructLayout(LayoutKind.Explicit)]
@ -162,13 +164,16 @@ namespace Avalonia.X11
Signaled?.Invoke(prio);
}
void HandleX11(CancellationToken cancellationToken)
unsafe void HandleX11(CancellationToken cancellationToken)
{
while (XPending(_display) != 0)
{
if (cancellationToken.IsCancellationRequested)
return;
XNextEvent(_display, out var xev);
if(XFilterEvent(ref xev, IntPtr.Zero))
continue;
if (xev.type == XEventName.GenericEvent)
XGetEventData(_display, &xev.GenericEventCookie);
try
@ -182,7 +187,7 @@ namespace Avalonia.X11
}
}
else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
handler(xev);
handler(ref xev);
}
finally
{

2
src/Avalonia.X11/X11Screens.cs

@ -76,7 +76,7 @@ namespace Avalonia.X11
XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify);
}
private void OnEvent(XEvent ev)
private void OnEvent(ref XEvent ev)
{
// Invalidate cache on RRScreenChangeNotify
if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify)

130
src/Avalonia.X11/X11Structs.cs

@ -53,7 +53,7 @@ namespace Avalonia.X11 {
internal struct XAnyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
}
@ -62,7 +62,7 @@ namespace Avalonia.X11 {
internal struct XKeyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -74,14 +74,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal int keycode;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XButtonEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -93,14 +93,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal int button;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMotionEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -112,14 +112,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal byte is_hint;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XCrossingEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -131,8 +131,8 @@ namespace Avalonia.X11 {
internal int y_root;
internal NotifyMode mode;
internal NotifyDetail detail;
internal bool same_screen;
internal bool focus;
internal int same_screen;
internal int focus;
internal XModifierMask state;
}
@ -140,7 +140,7 @@ namespace Avalonia.X11 {
internal struct XFocusChangeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int mode;
@ -151,7 +151,7 @@ namespace Avalonia.X11 {
internal struct XKeymapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal byte key_vector0;
@ -192,7 +192,7 @@ namespace Avalonia.X11 {
internal struct XExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int x;
@ -206,7 +206,7 @@ namespace Avalonia.X11 {
internal struct XGraphicsExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr drawable;
internal int x;
@ -222,7 +222,7 @@ namespace Avalonia.X11 {
internal struct XNoExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr drawable;
internal int major_code;
@ -233,7 +233,7 @@ namespace Avalonia.X11 {
internal struct XVisibilityEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int state;
@ -243,7 +243,7 @@ namespace Avalonia.X11 {
internal struct XCreateWindowEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -252,14 +252,14 @@ namespace Avalonia.X11 {
internal int width;
internal int height;
internal int border_width;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XDestroyWindowEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -269,29 +269,29 @@ namespace Avalonia.X11 {
internal struct XUnmapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal bool from_configure;
internal int from_configure;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMapRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -301,21 +301,21 @@ namespace Avalonia.X11 {
internal struct XReparentEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal IntPtr parent;
internal int x;
internal int y;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XConfigureEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -325,14 +325,14 @@ namespace Avalonia.X11 {
internal int height;
internal int border_width;
internal IntPtr above;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XGravityEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -344,7 +344,7 @@ namespace Avalonia.X11 {
internal struct XResizeRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int width;
@ -355,7 +355,7 @@ namespace Avalonia.X11 {
internal struct XConfigureRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -373,7 +373,7 @@ namespace Avalonia.X11 {
internal struct XCirculateEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -384,7 +384,7 @@ namespace Avalonia.X11 {
internal struct XCirculateRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -395,7 +395,7 @@ namespace Avalonia.X11 {
internal struct XPropertyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr atom;
@ -407,7 +407,7 @@ namespace Avalonia.X11 {
internal struct XSelectionClearEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr selection;
@ -418,7 +418,7 @@ namespace Avalonia.X11 {
internal struct XSelectionRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr owner;
internal IntPtr requestor;
@ -432,7 +432,7 @@ namespace Avalonia.X11 {
internal struct XSelectionEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr requestor;
internal IntPtr selection;
@ -445,11 +445,11 @@ namespace Avalonia.X11 {
internal struct XColormapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr colormap;
internal bool c_new;
internal int c_new;
internal int state;
}
@ -457,7 +457,7 @@ namespace Avalonia.X11 {
internal struct XClientMessageEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr message_type;
@ -473,7 +473,7 @@ namespace Avalonia.X11 {
internal struct XMappingEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int request;
@ -518,6 +518,15 @@ namespace Avalonia.X11 {
internal IntPtr pad21;
internal IntPtr pad22;
internal IntPtr pad23;
internal IntPtr pad24;
internal IntPtr pad25;
internal IntPtr pad26;
internal IntPtr pad27;
internal IntPtr pad28;
internal IntPtr pad29;
internal IntPtr pad30;
internal IntPtr pad31;
internal IntPtr pad32;
}
[StructLayout(LayoutKind.Sequential)]
@ -525,7 +534,7 @@ namespace Avalonia.X11 {
{
internal int type; /* of event. Always GenericEvent */
internal IntPtr serial; /* # of last request processed */
internal bool send_event; /* true if from SendEvent request */
internal int send_event; /* true if from SendEvent request */
internal IntPtr display; /* Display the event was read from */
internal int extension; /* major opcode of extension that caused the event */
internal int evtype; /* actual event type. */
@ -672,10 +681,10 @@ namespace Avalonia.X11 {
internal int backing_store;
internal IntPtr backing_planes;
internal IntPtr backing_pixel;
internal bool save_under;
internal int save_under;
internal IntPtr event_mask;
internal IntPtr do_not_propagate_mask;
internal bool override_redirect;
internal int override_redirect;
internal IntPtr colormap;
internal IntPtr cursor;
}
@ -696,14 +705,14 @@ namespace Avalonia.X11 {
internal int backing_store;
internal IntPtr backing_planes;
internal IntPtr backing_pixel;
internal bool save_under;
internal int save_under;
internal IntPtr colormap;
internal bool map_installed;
internal int map_installed;
internal MapState map_state;
internal IntPtr all_event_masks;
internal IntPtr your_event_mask;
internal IntPtr do_not_propagate_mask;
internal bool override_direct;
internal int override_direct;
internal IntPtr screen;
public override string ToString ()
@ -1029,7 +1038,7 @@ namespace Avalonia.X11 {
internal int max_maps;
internal int min_maps;
internal int backing_store;
internal bool save_unders;
internal int save_unders;
internal IntPtr root_input_mask;
}
@ -1280,7 +1289,7 @@ namespace Avalonia.X11 {
internal int ts_y_origin;
internal IntPtr font;
internal GCSubwindowMode subwindow_mode;
internal bool graphics_exposures;
internal int graphics_exposures;
internal int clip_x_origin;
internal int clib_y_origin;
internal IntPtr clip_mask;
@ -1499,7 +1508,7 @@ namespace Avalonia.X11 {
[StructLayout(LayoutKind.Sequential)]
internal struct XWMHints {
internal IntPtr flags;
internal bool input;
internal int input;
internal XInitialState initial_state;
internal IntPtr icon_pixmap;
internal IntPtr icon_window;
@ -1708,19 +1717,30 @@ namespace Avalonia.X11 {
}
[StructLayout (LayoutKind.Sequential)]
internal struct XIMStyles
internal unsafe struct XIMStyles
{
public ushort count_styles;
public IntPtr supported_styles;
public IntPtr* supported_styles;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
internal class XPoint
internal struct XPoint
{
public short X;
public short Y;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
internal struct XRectangle
{
public short X;
public short Y;
public short W;
public short H;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
@ -1798,7 +1818,7 @@ namespace Avalonia.X11 {
{
public ushort Length;
public IntPtr Feedback; // to XIMFeedbackStruct
public bool EncodingIsWChar;
public int EncodingIsWChar;
public IntPtr String; // it could be either char* or wchar_t*
}
@ -1850,6 +1870,8 @@ namespace Avalonia.X11 {
public const string XNClientWindow = "clientWindow";
public const string XNInputStyle = "inputStyle";
public const string XNFocusWindow = "focusWindow";
public const string XNResourceName = "resourceName";
public const string XNResourceClass = "resourceClass";
// XIMPreeditCallbacks delegate names.
public const string XNPreeditStartCallback = "preeditStartCallback";

208
src/Avalonia.X11/X11Window.Ime.cs

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.FreeDesktop;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Interop;
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)>();
unsafe void CreateIC()
{
if (_x11.HasXim)
{
XGetIMValues(_x11.Xim, XNames.XNQueryInputStyle, out var supported_styles, IntPtr.Zero);
for (var c = 0; c < supported_styles->count_styles; c++)
{
var style = (XIMProperties)supported_styles->supported_styles[c];
if ((int)(style & XIMProperties.XIMPreeditPosition) != 0
&& ((int)(style & XIMProperties.XIMStatusNothing) != 0))
{
XPoint spot = default;
XRectangle area = default;
//using var areaS = new Utf8Buffer("area");
using var spotS = new Utf8Buffer("spotLocation");
using var fontS = new Utf8Buffer("fontSet");
var list = XVaCreateNestedList(0,
//areaS, &area,
spotS, &spot,
fontS, _x11.DefaultFontSet,
IntPtr.Zero);
_xic = XCreateIC(_x11.Xim,
XNames.XNClientWindow, _handle,
XNames.XNFocusWindow, _handle,
XNames.XNInputStyle, new IntPtr((int)style),
XNames.XNResourceName, _platform.Options.WmClass,
XNames.XNResourceClass, _platform.Options.WmClass,
XNames.XNPreeditAttributes, list,
IntPtr.Zero);
XFree(list);
break;
}
}
XFree(new IntPtr(supported_styles));
}
if (_xic == IntPtr.Zero)
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle,
new IntPtr((int)(XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing)),
XNames.XNClientWindow, _handle, XNames.XNFocusWindow, _handle, IntPtr.Zero);
}
void InitializeIme()
{
var ime = AvaloniaLocator.Current.GetService<IX11InputMethodFactory>()?.CreateClient(_handle);
if (ime == null && _x11.HasXim)
{
var xim = new XimInputMethod(this);
ime = (xim, xim);
}
if (ime != null)
{
(_ime, _imeControl) = ime.Value;
_imeControl.Commit += s =>
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
_inputRoot, s));
_imeControl.ForwardKey += 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);
void HandleKeyEvent(ref 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 (ev.type == XEventName.KeyPress && !filtered)
TriggerClassicTextInputEvent(ref ev);
}
void TriggerClassicTextInputEvent(ref XEvent ev)
{
var text = TranslateEventToString(ref ev);
if (text != null)
ScheduleInput(
new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
ref ev);
}
private const int ImeBufferSize = 64 * 1024;
[ThreadStatic] private static IntPtr ImeBuffer;
unsafe string TranslateEventToString(ref XEvent ev)
{
if (ImeBuffer == IntPtr.Zero)
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize);
var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize,
out _, out var istatus);
var status = (XLookupStatus)istatus;
if (len == 0)
return null;
string text;
if (status == XLookupStatus.XBufferOverflow)
return null;
else
text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len);
if (text == null)
return null;
if (text.Length == 1)
{
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
return null;
}
return text;
}
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(ref ev.xev);
}
}
}
finally
{
_processingIme = false;
}
}
}
}

121
src/Avalonia.X11/X11Window.Xim.cs

@ -0,0 +1,121 @@
using System;
using System.Threading.Tasks;
using Avalonia.FreeDesktop;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
{
partial class X11Window
{
class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl
{
private readonly X11Window _parent;
private bool _controlActive, _windowActive, _imeActive;
private Rect? _queuedCursorRect;
public XimInputMethod(X11Window parent)
{
_parent = parent;
}
public void SetCursorRect(Rect rect)
{
var needEnqueue = _queuedCursorRect == null;
_queuedCursorRect = rect;
if(needEnqueue)
Dispatcher.UIThread.Post(() =>
{
if(_queuedCursorRect == null)
return;
var rc = _queuedCursorRect.Value;
_queuedCursorRect = null;
if (_parent._xic == IntPtr.Zero)
return;
rect *= _parent._scaling;
var pt = new XPoint
{
X = (short)Math.Min(Math.Max(rect.X, short.MinValue), short.MaxValue),
Y = (short)Math.Min(Math.Max(rect.Y + rect.Height, short.MinValue), short.MaxValue)
};
using var spotLoc = new Utf8Buffer(XNames.XNSpotLocation);
var list = XVaCreateNestedList(0, spotLoc, ref pt, IntPtr.Zero);
XSetICValues(_parent._xic, XNames.XNPreeditAttributes, list, IntPtr.Zero);
XFree(list);
}, DispatcherPriority.Background);
}
public void SetWindowActive(bool active)
{
_windowActive = active;
UpdateActive();
}
public void SetActive(bool active)
{
_controlActive = active;
UpdateActive();
}
private void UpdateActive()
{
var active = _windowActive && _controlActive;
if(_parent._xic == IntPtr.Zero)
return;
if (active != _imeActive)
{
_imeActive = active;
if (active)
{
Reset();
XSetICFocus(_parent._xic);
}
else
XUnsetICFocus(_parent._xic);
}
}
public void UpdateWindowInfo(PixelPoint position, double scaling)
{
// No-op
}
public void SetOptions(TextInputOptionsQueryEventArgs options)
{
// No-op
}
public void Reset()
{
if(_parent._xic == IntPtr.Zero)
return;
var data = XmbResetIC(_parent._xic);
if (data != IntPtr.Zero)
XFree(data);
}
public void Dispose()
{
// No-op
}
public bool IsEnabled => false;
public ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) =>
new ValueTask<bool>(false);
public event Action<string> Commit;
public event Action<X11InputMethodForwardedKey> ForwardKey;
}
}
}

80
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;
@ -79,7 +82,7 @@ namespace Avalonia.X11
if (_popup)
{
attr.override_redirect = true;
attr.override_redirect = 1;
valueMask |= SetWindowValuemask.OverrideRedirect;
}
@ -178,11 +181,12 @@ 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);
CreateIC();
XFlush(_x11.Display);
if(_popup)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
@ -194,6 +198,7 @@ namespace Avalonia.X11
Paint?.Invoke(default);
return _handle != IntPtr.Zero;
}, TimeSpan.FromMilliseconds(100));
InitializeIme();
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@ -350,15 +355,13 @@ namespace Avalonia.X11
(IRenderer)new X11ImmediateRendererProxy(root, loop);
}
void OnEvent(XEvent ev)
void OnEvent(ref XEvent ev)
{
lock (SyncRoot)
OnEventSync(ev);
OnEventSync(ref ev);
}
void OnEventSync(XEvent ev)
void OnEventSync(ref XEvent ev)
{
if(XFilterEvent(ref ev, _handle))
return;
if (ev.type == XEventName.MapNotify)
{
_mapped = true;
@ -386,9 +389,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)
@ -447,7 +454,7 @@ namespace Avalonia.X11
return;
var needEnqueue = (_configure == null);
_configure = ev.ConfigureEvent;
if (ev.ConfigureEvent.override_redirect || ev.ConfigureEvent.send_event)
if (ev.ConfigureEvent.override_redirect != 0 || ev.ConfigureEvent.send_event != 0)
_configurePoint = new PixelPoint(ev.ConfigureEvent.x, ev.ConfigureEvent.y);
else
{
@ -477,6 +484,7 @@ namespace Avalonia.X11
PositionChanged?.Invoke(npos);
updatedSizeViaScaling = UpdateScaling();
}
UpdateImePosition();
if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize);
@ -487,7 +495,8 @@ namespace Avalonia.X11
XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width,
ev.ConfigureEvent.height);
}
else if (ev.type == XEventName.DestroyNotify && ev.AnyEvent.window == _handle)
else if (ev.type == XEventName.DestroyNotify
&& ev.DestroyWindowEvent.window == _handle)
{
Cleanup();
}
@ -507,39 +516,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(ref ev);
}
}
@ -562,6 +539,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 +677,7 @@ namespace Avalonia.X11
_x11.LastActivityTimestamp = xev.ButtonEvent.time;
ScheduleInput(args);
}
public void ScheduleXI2Input(RawInputEventArgs args)
{
@ -781,6 +760,13 @@ namespace Avalonia.X11
void Cleanup()
{
if (_imeControl != null)
{
_imeControl.Dispose();
_imeControl = null;
_ime = null;
}
if (_xic != IntPtr.Zero)
{
XDestroyIC(_xic);
@ -957,7 +943,7 @@ namespace Avalonia.X11
ClientMessageEvent =
{
type = XEventName.ClientMessage,
send_event = true,
send_event = 1,
window = _handle,
message_type = message_type,
format = 32,
@ -1130,6 +1116,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);

66
src/Avalonia.X11/XLib.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Platform.Interop;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable FieldCanBeMadeReadOnly.Global
@ -57,6 +58,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent);
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent);
[DllImport(libX11)]
public static extern int XConnectionNumber(IntPtr diplay);
@ -407,6 +411,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern bool XFilterEvent(ref XEvent xevent, IntPtr window);
[DllImport(libX11)]
public static extern bool XFilterEvent(XEvent* xevent, IntPtr window);
[DllImport(libX11)]
public static extern void XkbSetDetectableAutoRepeat(IntPtr display, bool detectable, IntPtr supported);
@ -441,9 +448,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern IntPtr XCreateColormap(IntPtr display, IntPtr window, IntPtr visual, int create);
public enum XLookupStatus
public enum XLookupStatus : uint
{
XBufferOverflow = -1,
XBufferOverflow = 0xffffffffu,
XLookupNone = 1,
XLookupChars = 2,
XLookupKeySym = 3,
@ -454,7 +461,10 @@ namespace Avalonia.X11
public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out UIntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe IntPtr XKeycodeToKeysym(IntPtr display, int keycode, int index);
@ -464,12 +474,52 @@ namespace Avalonia.X11
[DllImport (libX11)]
public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, IntPtr terminator);
public static extern IntPtr XGetIMValues (IntPtr xim, string name, out XIMStyles* value, IntPtr terminator);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, IntPtr value, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string name, IntPtr value, string name2, IntPtr value2,
string name3, IntPtr value3, string name4, IntPtr value4, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle,
string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, string xnFocusWindow,
IntPtr value2, string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
[DllImport(libX11)]
public static extern void XSetICFocus(IntPtr xic);
[DllImport(libX11)]
public static extern void XUnsetICFocus(IntPtr xic);
[DllImport(libX11)]
public static extern IntPtr XmbResetIC(IntPtr xic);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer name, ref XPoint point, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer xnArea, XRectangle* point,
Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused,
Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
public static extern IntPtr XCreateFontSet (IntPtr display, string name, out IntPtr list, out int count, IntPtr unused);
[DllImport(libX11)]
public static extern IntPtr XSetICValues(IntPtr ic, string name, IntPtr data, IntPtr terminator);
[DllImport (libX11)]
public static extern void XCloseIM (IntPtr xim);
@ -633,14 +683,12 @@ namespace Avalonia.X11
}
}
public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, Action<XEvent> handler)
public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler)
{
var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow,
0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);
plat.Windows[win] = handler;
return win;
}
}
}

Loading…
Cancel
Save