DBusMenuExporter.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.IO;
  5. using System.Reactive.Disposables;
  6. using System.Threading.Tasks;
  7. using Avalonia.Controls;
  8. using Avalonia.Controls.Platform;
  9. using Avalonia.FreeDesktop.DBusMenu;
  10. using Avalonia.Input;
  11. using Avalonia.Threading;
  12. using Tmds.DBus;
  13. #pragma warning disable 1998
  14. namespace Avalonia.FreeDesktop
  15. {
  16. public class DBusMenuExporter
  17. {
  18. public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid)
  19. {
  20. if (DBusHelper.Connection == null)
  21. return null;
  22. return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
  23. }
  24. class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
  25. {
  26. private readonly Connection _dbus;
  27. private readonly uint _xid;
  28. private IRegistrar _registar;
  29. private bool _disposed;
  30. private uint _revision = 1;
  31. private NativeMenu _menu;
  32. private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
  33. private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
  34. private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
  35. private bool _resetQueued;
  36. private int _nextId = 1;
  37. public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
  38. {
  39. _dbus = dbus;
  40. _xid = (uint)xid.ToInt32();
  41. ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/"
  42. + Guid.NewGuid().ToString().Replace("-", ""));
  43. SetNativeMenu(new NativeMenu());
  44. Init();
  45. }
  46. async void Init()
  47. {
  48. try
  49. {
  50. await _dbus.RegisterObjectAsync(this);
  51. _registar = DBusHelper.Connection.CreateProxy<IRegistrar>(
  52. "com.canonical.AppMenu.Registrar",
  53. "/com/canonical/AppMenu/Registrar");
  54. if (!_disposed)
  55. await _registar.RegisterWindowAsync(_xid, ObjectPath);
  56. }
  57. catch (Exception e)
  58. {
  59. Console.Error.WriteLine(e);
  60. // It's not really important if this code succeeds,
  61. // and it's not important to know if it succeeds
  62. // since even if we register the window it's not guaranteed that
  63. // menu will be actually exported
  64. }
  65. }
  66. public void Dispose()
  67. {
  68. if (_disposed)
  69. return;
  70. _disposed = true;
  71. _dbus.UnregisterObject(this);
  72. // Fire and forget
  73. _registar?.UnregisterWindowAsync(_xid);
  74. }
  75. public bool IsNativeMenuExported { get; private set; }
  76. public event EventHandler OnIsNativeMenuExportedChanged;
  77. public void SetNativeMenu(NativeMenu menu)
  78. {
  79. if (menu == null)
  80. menu = new NativeMenu();
  81. if (_menu != null)
  82. ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
  83. _menu = menu;
  84. ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
  85. DoLayoutReset();
  86. }
  87. /*
  88. This is basic initial implementation, so we don't actually track anything and
  89. just reset the whole layout on *ANY* change
  90. This is not how it should work and will prevent us from implementing various features,
  91. but that's the fastest way to get things working, so...
  92. */
  93. void DoLayoutReset()
  94. {
  95. _resetQueued = false;
  96. foreach (var i in _idsToItems.Values)
  97. i.PropertyChanged -= OnItemPropertyChanged;
  98. foreach(var menu in _menus)
  99. ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged;
  100. _menus.Clear();
  101. _idsToItems.Clear();
  102. _itemsToIds.Clear();
  103. _revision++;
  104. LayoutUpdated?.Invoke((_revision, 0));
  105. }
  106. void QueueReset()
  107. {
  108. if(_resetQueued)
  109. return;
  110. _resetQueued = true;
  111. Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
  112. }
  113. private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id)
  114. {
  115. if (id == 0)
  116. return (null, _menu);
  117. _idsToItems.TryGetValue(id, out var item);
  118. return (item, (item as NativeMenuItem)?.Menu);
  119. }
  120. private void EnsureSubscribed(NativeMenu menu)
  121. {
  122. if(menu!=null && _menus.Add(menu))
  123. ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged;
  124. }
  125. private int GetId(NativeMenuItemBase item)
  126. {
  127. if (_itemsToIds.TryGetValue(item, out var id))
  128. return id;
  129. id = _nextId++;
  130. _idsToItems[id] = item;
  131. _itemsToIds[item] = id;
  132. item.PropertyChanged += OnItemPropertyChanged;
  133. if (item is NativeMenuItem nmi)
  134. EnsureSubscribed(nmi.Menu);
  135. return id;
  136. }
  137. private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
  138. {
  139. QueueReset();
  140. }
  141. private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
  142. {
  143. QueueReset();
  144. }
  145. public ObjectPath ObjectPath { get; }
  146. async Task<object> IFreeDesktopDBusProperties.GetAsync(string prop)
  147. {
  148. if (prop == "Version")
  149. return 2;
  150. if (prop == "Status")
  151. return "normal";
  152. return 0;
  153. }
  154. async Task<DBusMenuProperties> IFreeDesktopDBusProperties.GetAllAsync()
  155. {
  156. return new DBusMenuProperties
  157. {
  158. Version = 2,
  159. Status = "normal",
  160. };
  161. }
  162. private static string[] AllProperties = new[]
  163. {
  164. "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
  165. };
  166. object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
  167. {
  168. var (it, menu) = i;
  169. if (it is NativeMenuItemSeperator)
  170. {
  171. if (name == "type")
  172. return "separator";
  173. }
  174. else if (it is NativeMenuItem item)
  175. {
  176. if (name == "type")
  177. {
  178. return null;
  179. }
  180. if (name == "label")
  181. return item?.Header ?? "<null>";
  182. if (name == "enabled")
  183. {
  184. if (item == null)
  185. return null;
  186. if (item.Menu != null && item.Menu.Items.Count == 0)
  187. return false;
  188. if (item.IsEnabled == false)
  189. return false;
  190. return null;
  191. }
  192. if (name == "shortcut")
  193. {
  194. if (item?.Gesture == null)
  195. return null;
  196. if (item.Gesture.KeyModifiers == 0)
  197. return null;
  198. var lst = new List<string>();
  199. var mod = item.Gesture;
  200. if ((mod.KeyModifiers & KeyModifiers.Control) != 0)
  201. lst.Add("Control");
  202. if ((mod.KeyModifiers & KeyModifiers.Alt) != 0)
  203. lst.Add("Alt");
  204. if ((mod.KeyModifiers & KeyModifiers.Shift) != 0)
  205. lst.Add("Shift");
  206. if ((mod.KeyModifiers & KeyModifiers.Meta) != 0)
  207. lst.Add("Super");
  208. lst.Add(item.Gesture.Key.ToString());
  209. return new[] { lst.ToArray() };
  210. }
  211. if (name == "toggle-type")
  212. {
  213. if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
  214. return "checkmark";
  215. if (item.ToggleType == NativeMenuItemToggleType.Radio)
  216. return "radio";
  217. }
  218. if (name == "toggle-state")
  219. {
  220. if (item.ToggleType != NativeMenuItemToggleType.None)
  221. return item.IsChecked ? 1 : 0;
  222. }
  223. if (name == "icon-data")
  224. {
  225. if (item.Icon != null)
  226. {
  227. var ms = new MemoryStream();
  228. item.Icon.Save(ms);
  229. return ms.ToArray();
  230. }
  231. }
  232. if (name == "children-display")
  233. return menu != null ? "submenu" : null;
  234. }
  235. return null;
  236. }
  237. private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
  238. KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names)
  239. {
  240. if (names?.Length > 0 != true)
  241. names = AllProperties;
  242. _reusablePropertyList.Clear();
  243. foreach (var n in names)
  244. {
  245. var v = GetProperty(i, n);
  246. if (v != null)
  247. _reusablePropertyList.Add(new KeyValuePair<string, object>(n, v));
  248. }
  249. return _reusablePropertyList.ToArray();
  250. }
  251. public Task SetAsync(string prop, object val) => Task.CompletedTask;
  252. public Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(
  253. int ParentId, int RecursionDepth, string[] PropertyNames)
  254. {
  255. var menu = GetMenu(ParentId);
  256. var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames));
  257. if (!IsNativeMenuExported)
  258. {
  259. IsNativeMenuExported = true;
  260. Dispatcher.UIThread.Post(() =>
  261. {
  262. OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty);
  263. });
  264. }
  265. return Task.FromResult(rv);
  266. }
  267. (int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames)
  268. {
  269. var id = item == null ? 0 : GetId(item);
  270. var props = GetProperties((item, menu), propertyNames);
  271. var children = (depth == 0 || menu == null) ? new object[0] : new object[menu.Items.Count];
  272. if(menu != null)
  273. for (var c = 0; c < children.Length; c++)
  274. {
  275. var ch = menu.Items[c];
  276. children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames);
  277. }
  278. return (id, props, children);
  279. }
  280. public Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames)
  281. {
  282. var arr = new (int, KeyValuePair<string, object>[])[Ids.Length];
  283. for (var c = 0; c < Ids.Length; c++)
  284. {
  285. var id = Ids[c];
  286. var item = GetMenu(id);
  287. var props = GetProperties(item, PropertyNames);
  288. arr[c] = (id, props);
  289. }
  290. return Task.FromResult(arr);
  291. }
  292. public async Task<object> GetPropertyAsync(int Id, string Name)
  293. {
  294. return GetProperty(GetMenu(Id), Name) ?? 0;
  295. }
  296. public void HandleEvent(int id, string eventId, object data, uint timestamp)
  297. {
  298. if (eventId == "clicked")
  299. {
  300. var item = GetMenu(id).item;
  301. if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
  302. {
  303. if (menuItem?.IsEnabled == true)
  304. bridge?.RaiseClicked();
  305. }
  306. }
  307. }
  308. public Task EventAsync(int Id, string EventId, object Data, uint Timestamp)
  309. {
  310. HandleEvent(Id, EventId, Data, Timestamp);
  311. return Task.CompletedTask;
  312. }
  313. public Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events)
  314. {
  315. foreach (var e in Events)
  316. HandleEvent(e.id, e.eventId, e.data, e.timestamp);
  317. return Task.FromResult(new int[0]);
  318. }
  319. public async Task<bool> AboutToShowAsync(int Id)
  320. {
  321. return false;
  322. }
  323. public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids)
  324. {
  325. return (new int[0], new int[0]);
  326. }
  327. #region Events
  328. private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)>
  329. ItemsPropertiesUpdated;
  330. private event Action<(uint revision, int parent)> LayoutUpdated;
  331. private event Action<(int id, uint timestamp)> ItemActivationRequested;
  332. private event Action<PropertyChanges> PropertiesChanged;
  333. async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError)
  334. {
  335. ItemsPropertiesUpdated += handler;
  336. return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
  337. }
  338. async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError)
  339. {
  340. LayoutUpdated += handler;
  341. return Disposable.Create(() => LayoutUpdated -= handler);
  342. }
  343. async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError)
  344. {
  345. ItemActivationRequested+= handler;
  346. return Disposable.Create(() => ItemActivationRequested -= handler);
  347. }
  348. async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> handler)
  349. {
  350. PropertiesChanged += handler;
  351. return Disposable.Create(() => PropertiesChanged -= handler);
  352. }
  353. #endregion
  354. }
  355. }
  356. }