29 changed files with 1265 additions and 53 deletions
Binary file not shown.
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 |
|||
}; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
namespace Avalonia.Input.TextInput |
|||
{ |
|||
public enum TextInputContentType |
|||
{ |
|||
Normal = 0, |
|||
Email = 1, |
|||
Phone = 2, |
|||
Number = 3, |
|||
Url = 4, |
|||
Password = 5 |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue