csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
9.0 KiB
293 lines
9.0 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Input.TextInput;
|
|
using Avalonia.Logging;
|
|
using Tmds.DBus.Protocol;
|
|
|
|
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;
|
|
private ITextInputMethodClient? _client;
|
|
|
|
protected bool IsConnected => _currentName != null;
|
|
|
|
public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
|
|
{
|
|
_queue = new DBusCallQueue(QueueOnErrorAsync);
|
|
Connection = connection;
|
|
_knownNames = knownNames;
|
|
Watch();
|
|
}
|
|
|
|
public ITextInputMethodClient Client => _client;
|
|
|
|
public bool IsActive => _client is not null;
|
|
|
|
private async void Watch()
|
|
{
|
|
foreach (var name in _knownNames)
|
|
{
|
|
var dbus = new OrgFreedesktopDBus(Connection, name, "/org/freedesktop/DBus");
|
|
_disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange));
|
|
var nameOwner = await dbus.GetNameOwnerAsync(name);
|
|
OnNameChange(null, (name, null, nameOwner));
|
|
}
|
|
}
|
|
|
|
protected abstract Task<bool> Connect(string name);
|
|
|
|
protected string GetAppName() =>
|
|
Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
|
|
|
|
private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args)
|
|
{
|
|
if (args.NewOwner is not null && _currentName is 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 ex)
|
|
{
|
|
Logger.TryGet(LogEventLevel.Error, "IME")
|
|
?.Log(this, "Unable to create IME input context:\n" + ex);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_connecting = false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// IME has crashed
|
|
if (args.NewOwner is null && args.ServiceName == _currentName)
|
|
{
|
|
_currentName = null;
|
|
foreach (var s in _disposables)
|
|
s.Dispose();
|
|
_disposables.Clear();
|
|
|
|
OnDisconnected();
|
|
Reset();
|
|
|
|
// Watch again
|
|
Watch();
|
|
}
|
|
}
|
|
|
|
protected virtual Task DisconnectAsync()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
protected virtual void OnDisconnected()
|
|
{
|
|
|
|
}
|
|
|
|
protected virtual void Reset()
|
|
{
|
|
_lastReportedRect = null;
|
|
_imeActive = null;
|
|
}
|
|
|
|
private async Task QueueOnErrorAsync(Exception e)
|
|
{
|
|
Logger.TryGet(LogEventLevel.Error, "IME")
|
|
?.Log(this, "Error:\n" + e);
|
|
try
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
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)
|
|
{
|
|
if(d is { })
|
|
_disposables.Add(d);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach(var d in _disposables)
|
|
d.Dispose();
|
|
_disposables.Clear();
|
|
DisconnectAsync();
|
|
_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);
|
|
|
|
private 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.SetClient(ITextInputMethodClient? client)
|
|
{
|
|
_client = client;
|
|
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 QueueOnErrorAsync(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);
|
|
|
|
private 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(TextInputOptions options);
|
|
|
|
void ITextInputMethodImpl.Reset()
|
|
{
|
|
Reset();
|
|
_queue.Enqueue(async () =>
|
|
{
|
|
if (!IsConnected)
|
|
return;
|
|
await ResetContextCore();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|