DBusTextInputMethodBase.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Reflection;
  5. using System.Threading.Tasks;
  6. using Avalonia.FreeDesktop.DBusIme.Fcitx;
  7. using Avalonia.Input.Raw;
  8. using Avalonia.Input.TextInput;
  9. using Avalonia.Logging;
  10. using Tmds.DBus;
  11. namespace Avalonia.FreeDesktop.DBusIme
  12. {
  13. internal class DBusInputMethodFactory<T> : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
  14. {
  15. private readonly Func<IntPtr, T> _factory;
  16. public DBusInputMethodFactory(Func<IntPtr, T> factory)
  17. {
  18. _factory = factory;
  19. }
  20. public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
  21. {
  22. var im = _factory(xid);
  23. return (im, im);
  24. }
  25. }
  26. internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
  27. {
  28. private List<IDisposable> _disposables = new List<IDisposable>();
  29. private Queue<string> _onlineNamesQueue = new Queue<string>();
  30. protected Connection Connection { get; }
  31. private readonly string[] _knownNames;
  32. private bool _connecting;
  33. private string _currentName;
  34. private DBusCallQueue _queue;
  35. private bool _controlActive, _windowActive;
  36. private bool? _imeActive;
  37. private Rect _logicalRect;
  38. private PixelRect? _lastReportedRect;
  39. private double _scaling = 1;
  40. private PixelPoint _windowPosition;
  41. protected bool IsConnected => _currentName != null;
  42. public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
  43. {
  44. _queue = new DBusCallQueue(QueueOnError);
  45. Connection = connection;
  46. _knownNames = knownNames;
  47. Watch();
  48. }
  49. async void Watch()
  50. {
  51. foreach (var name in _knownNames)
  52. _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
  53. }
  54. protected abstract Task<bool> Connect(string name);
  55. protected string GetAppName() =>
  56. Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
  57. private async void OnNameChange(ServiceOwnerChangedEventArgs args)
  58. {
  59. if (args.NewOwner != null && _currentName == null)
  60. {
  61. _onlineNamesQueue.Enqueue(args.ServiceName);
  62. if(!_connecting)
  63. {
  64. _connecting = true;
  65. try
  66. {
  67. while (_onlineNamesQueue.Count > 0)
  68. {
  69. var name = _onlineNamesQueue.Dequeue();
  70. try
  71. {
  72. if (await Connect(name))
  73. {
  74. _onlineNamesQueue.Clear();
  75. _currentName = name;
  76. return;
  77. }
  78. }
  79. catch (Exception e)
  80. {
  81. Logger.TryGet(LogEventLevel.Error, "IME")
  82. ?.Log(this, "Unable to create IME input context:\n" + e);
  83. }
  84. }
  85. }
  86. finally
  87. {
  88. _connecting = false;
  89. }
  90. }
  91. }
  92. // IME has crashed
  93. if (args.NewOwner == null && args.ServiceName == _currentName)
  94. {
  95. _currentName = null;
  96. foreach(var s in _disposables)
  97. s.Dispose();
  98. _disposables.Clear();
  99. OnDisconnected();
  100. Reset();
  101. // Watch again
  102. Watch();
  103. }
  104. }
  105. protected virtual Task Disconnect()
  106. {
  107. return Task.CompletedTask;
  108. }
  109. protected virtual void OnDisconnected()
  110. {
  111. }
  112. protected virtual void Reset()
  113. {
  114. _lastReportedRect = null;
  115. _imeActive = null;
  116. }
  117. async Task QueueOnError(Exception e)
  118. {
  119. Logger.TryGet(LogEventLevel.Error, "IME")
  120. ?.Log(this, "Error:\n" + e);
  121. try
  122. {
  123. await Disconnect();
  124. }
  125. catch (Exception ex)
  126. {
  127. Logger.TryGet(LogEventLevel.Error, "IME")
  128. ?.Log(this, "Error while destroying the context:\n" + ex);
  129. }
  130. OnDisconnected();
  131. _currentName = null;
  132. }
  133. protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb);
  134. protected void AddDisposable(IDisposable d) => _disposables.Add(d);
  135. public void Dispose()
  136. {
  137. foreach(var d in _disposables)
  138. d.Dispose();
  139. _disposables.Clear();
  140. try
  141. {
  142. Disconnect().ContinueWith(_ => { });
  143. }
  144. catch
  145. {
  146. // fire and forget
  147. }
  148. _currentName = null;
  149. }
  150. protected abstract Task SetCursorRectCore(PixelRect rect);
  151. protected abstract Task SetActiveCore(bool active);
  152. protected abstract Task ResetContextCore();
  153. protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
  154. void UpdateActive()
  155. {
  156. _queue.Enqueue(async () =>
  157. {
  158. if(!IsConnected)
  159. return;
  160. var active = _windowActive && _controlActive;
  161. if (active != _imeActive)
  162. {
  163. _imeActive = active;
  164. await SetActiveCore(active);
  165. }
  166. });
  167. }
  168. void IX11InputMethodControl.SetWindowActive(bool active)
  169. {
  170. _windowActive = active;
  171. UpdateActive();
  172. }
  173. void ITextInputMethodImpl.SetActive(bool active)
  174. {
  175. _controlActive = active;
  176. UpdateActive();
  177. }
  178. bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
  179. async ValueTask<bool> IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
  180. {
  181. try
  182. {
  183. return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode));
  184. }
  185. // Disconnected
  186. catch (OperationCanceledException)
  187. {
  188. return false;
  189. }
  190. // Error, disconnect
  191. catch (Exception e)
  192. {
  193. await QueueOnError(e);
  194. return false;
  195. }
  196. }
  197. private Action<string> _onCommit;
  198. event Action<string> IX11InputMethodControl.Commit
  199. {
  200. add => _onCommit += value;
  201. remove => _onCommit -= value;
  202. }
  203. protected void FireCommit(string s) => _onCommit?.Invoke(s);
  204. private Action<X11InputMethodForwardedKey> _onForward;
  205. event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
  206. {
  207. add => _onForward += value;
  208. remove => _onForward -= value;
  209. }
  210. protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
  211. void UpdateCursorRect()
  212. {
  213. _queue.Enqueue(async () =>
  214. {
  215. if(!IsConnected)
  216. return;
  217. var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
  218. cursorRect = cursorRect.Translate(_windowPosition);
  219. if (cursorRect != _lastReportedRect)
  220. {
  221. _lastReportedRect = cursorRect;
  222. await SetCursorRectCore(cursorRect);
  223. }
  224. });
  225. }
  226. void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
  227. {
  228. _windowPosition = position;
  229. _scaling = scaling;
  230. UpdateCursorRect();
  231. }
  232. void ITextInputMethodImpl.SetCursorRect(Rect rect)
  233. {
  234. _logicalRect = rect;
  235. UpdateCursorRect();
  236. }
  237. public abstract void SetOptions(TextInputOptionsQueryEventArgs options);
  238. void ITextInputMethodImpl.Reset()
  239. {
  240. Reset();
  241. _queue.Enqueue(async () =>
  242. {
  243. if (!IsConnected)
  244. return;
  245. await ResetContextCore();
  246. });
  247. }
  248. }
  249. }