| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- using System;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.IO;
- using System.Reactive.Disposables;
- using System.Threading.Tasks;
- using Avalonia.Controls;
- using Avalonia.Controls.Platform;
- using Avalonia.FreeDesktop.DBusMenu;
- using Avalonia.Input;
- using Avalonia.Threading;
- using Tmds.DBus;
- #pragma warning disable 1998
- namespace Avalonia.FreeDesktop
- {
- public class DBusMenuExporter
- {
- public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid)
- {
- if (DBusHelper.Connection == null)
- return null;
- return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
- }
- class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
- {
- private readonly Connection _dbus;
- private readonly uint _xid;
- private IRegistrar _registar;
- private bool _disposed;
- private uint _revision = 1;
- private NativeMenu _menu;
- private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
- private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
- private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
- private bool _resetQueued;
- private int _nextId = 1;
- public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
- {
- _dbus = dbus;
- _xid = (uint)xid.ToInt32();
- ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/"
- + Guid.NewGuid().ToString().Replace("-", ""));
- SetNativeMenu(new NativeMenu());
- Init();
- }
- async void Init()
- {
- try
- {
- await _dbus.RegisterObjectAsync(this);
- _registar = DBusHelper.Connection.CreateProxy<IRegistrar>(
- "com.canonical.AppMenu.Registrar",
- "/com/canonical/AppMenu/Registrar");
- if (!_disposed)
- await _registar.RegisterWindowAsync(_xid, ObjectPath);
- }
- catch (Exception e)
- {
- Console.Error.WriteLine(e);
- // It's not really important if this code succeeds,
- // and it's not important to know if it succeeds
- // since even if we register the window it's not guaranteed that
- // menu will be actually exported
- }
- }
- public void Dispose()
- {
- if (_disposed)
- return;
- _disposed = true;
- _dbus.UnregisterObject(this);
- // Fire and forget
- _registar?.UnregisterWindowAsync(_xid);
- }
- public bool IsNativeMenuExported { get; private set; }
- public event EventHandler OnIsNativeMenuExportedChanged;
- public void SetNativeMenu(NativeMenu menu)
- {
- if (menu == null)
- menu = new NativeMenu();
- if (_menu != null)
- ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
- _menu = menu;
- ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
-
- DoLayoutReset();
- }
-
- /*
- This is basic initial implementation, so we don't actually track anything and
- just reset the whole layout on *ANY* change
-
- This is not how it should work and will prevent us from implementing various features,
- but that's the fastest way to get things working, so...
- */
- void DoLayoutReset()
- {
- _resetQueued = false;
- foreach (var i in _idsToItems.Values)
- i.PropertyChanged -= OnItemPropertyChanged;
- foreach(var menu in _menus)
- ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged;
- _menus.Clear();
- _idsToItems.Clear();
- _itemsToIds.Clear();
- _revision++;
- LayoutUpdated?.Invoke((_revision, 0));
- }
- void QueueReset()
- {
- if(_resetQueued)
- return;
- _resetQueued = true;
- Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
- }
- private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id)
- {
- if (id == 0)
- return (null, _menu);
- _idsToItems.TryGetValue(id, out var item);
- return (item, (item as NativeMenuItem)?.Menu);
- }
- private void EnsureSubscribed(NativeMenu menu)
- {
- if(menu!=null && _menus.Add(menu))
- ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged;
- }
-
- private int GetId(NativeMenuItemBase item)
- {
- if (_itemsToIds.TryGetValue(item, out var id))
- return id;
- id = _nextId++;
- _idsToItems[id] = item;
- _itemsToIds[item] = id;
- item.PropertyChanged += OnItemPropertyChanged;
- if (item is NativeMenuItem nmi)
- EnsureSubscribed(nmi.Menu);
- return id;
- }
- private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- QueueReset();
- }
- private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
- {
- QueueReset();
- }
- public ObjectPath ObjectPath { get; }
- async Task<object> IFreeDesktopDBusProperties.GetAsync(string prop)
- {
- if (prop == "Version")
- return 2;
- if (prop == "Status")
- return "normal";
- return 0;
- }
- async Task<DBusMenuProperties> IFreeDesktopDBusProperties.GetAllAsync()
- {
- return new DBusMenuProperties
- {
- Version = 2,
- Status = "normal",
- };
- }
- private static string[] AllProperties = new[]
- {
- "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
- };
-
- object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
- {
- var (it, menu) = i;
- if (it is NativeMenuItemSeperator)
- {
- if (name == "type")
- return "separator";
- }
- else if (it is NativeMenuItem item)
- {
- if (name == "type")
- {
- return null;
- }
- if (name == "label")
- return item?.Header ?? "<null>";
- if (name == "enabled")
- {
- if (item == null)
- return null;
- if (item.Menu != null && item.Menu.Items.Count == 0)
- return false;
- if (item.IsEnabled == false)
- return false;
- return null;
- }
- if (name == "shortcut")
- {
- if (item?.Gesture == null)
- return null;
- if (item.Gesture.KeyModifiers == 0)
- return null;
- var lst = new List<string>();
- var mod = item.Gesture;
- if ((mod.KeyModifiers & KeyModifiers.Control) != 0)
- lst.Add("Control");
- if ((mod.KeyModifiers & KeyModifiers.Alt) != 0)
- lst.Add("Alt");
- if ((mod.KeyModifiers & KeyModifiers.Shift) != 0)
- lst.Add("Shift");
- if ((mod.KeyModifiers & KeyModifiers.Meta) != 0)
- lst.Add("Super");
- lst.Add(item.Gesture.Key.ToString());
- return new[] { lst.ToArray() };
- }
- if (name == "toggle-type")
- {
- if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
- return "checkmark";
- if (item.ToggleType == NativeMenuItemToggleType.Radio)
- return "radio";
- }
- if (name == "toggle-state")
- {
- if (item.ToggleType != NativeMenuItemToggleType.None)
- return item.IsChecked ? 1 : 0;
- }
- if (name == "icon-data")
- {
- if (item.Icon != null)
- {
- var ms = new MemoryStream();
- item.Icon.Save(ms);
- return ms.ToArray();
- }
- }
- if (name == "children-display")
- return menu != null ? "submenu" : null;
- }
- return null;
- }
- private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
- KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names)
- {
- if (names?.Length > 0 != true)
- names = AllProperties;
- _reusablePropertyList.Clear();
- foreach (var n in names)
- {
- var v = GetProperty(i, n);
- if (v != null)
- _reusablePropertyList.Add(new KeyValuePair<string, object>(n, v));
- }
- return _reusablePropertyList.ToArray();
- }
-
- public Task SetAsync(string prop, object val) => Task.CompletedTask;
- public Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(
- int ParentId, int RecursionDepth, string[] PropertyNames)
- {
- var menu = GetMenu(ParentId);
- var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames));
- if (!IsNativeMenuExported)
- {
- IsNativeMenuExported = true;
- Dispatcher.UIThread.Post(() =>
- {
- OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty);
- });
- }
- return Task.FromResult(rv);
- }
- (int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames)
- {
- var id = item == null ? 0 : GetId(item);
- var props = GetProperties((item, menu), propertyNames);
- var children = (depth == 0 || menu == null) ? new object[0] : new object[menu.Items.Count];
- if(menu != null)
- for (var c = 0; c < children.Length; c++)
- {
- var ch = menu.Items[c];
- children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames);
- }
- return (id, props, children);
- }
- public Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames)
- {
- var arr = new (int, KeyValuePair<string, object>[])[Ids.Length];
- for (var c = 0; c < Ids.Length; c++)
- {
- var id = Ids[c];
- var item = GetMenu(id);
- var props = GetProperties(item, PropertyNames);
- arr[c] = (id, props);
- }
- return Task.FromResult(arr);
- }
- public async Task<object> GetPropertyAsync(int Id, string Name)
- {
- return GetProperty(GetMenu(Id), Name) ?? 0;
- }
- public void HandleEvent(int id, string eventId, object data, uint timestamp)
- {
- if (eventId == "clicked")
- {
- var item = GetMenu(id).item;
- if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
- {
- if (menuItem?.IsEnabled == true)
- bridge?.RaiseClicked();
- }
- }
- }
-
- public Task EventAsync(int Id, string EventId, object Data, uint Timestamp)
- {
- HandleEvent(Id, EventId, Data, Timestamp);
- return Task.CompletedTask;
- }
- public Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events)
- {
- foreach (var e in Events)
- HandleEvent(e.id, e.eventId, e.data, e.timestamp);
- return Task.FromResult(new int[0]);
- }
- public async Task<bool> AboutToShowAsync(int Id)
- {
- return false;
- }
- public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids)
- {
- return (new int[0], new int[0]);
- }
- #region Events
- private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)>
- ItemsPropertiesUpdated;
- private event Action<(uint revision, int parent)> LayoutUpdated;
- private event Action<(int id, uint timestamp)> ItemActivationRequested;
- private event Action<PropertyChanges> PropertiesChanged;
- async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError)
- {
- ItemsPropertiesUpdated += handler;
- return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
- }
- async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError)
- {
- LayoutUpdated += handler;
- return Disposable.Create(() => LayoutUpdated -= handler);
- }
- async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError)
- {
- ItemActivationRequested+= handler;
- return Disposable.Create(() => ItemActivationRequested -= handler);
- }
- async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> handler)
- {
- PropertiesChanged += handler;
- return Disposable.Create(() => PropertiesChanged -= handler);
- }
- #endregion
- }
- }
- }
|