15 changed files with 935 additions and 4 deletions
@ -0,0 +1,100 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public partial class NativeMenu |
|||
{ |
|||
public static readonly AttachedProperty<bool> IsNativeMenuExportedProperty = |
|||
AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, bool>("IsNativeMenuExported", |
|||
defaultBindingMode: BindingMode.OneWayToSource); |
|||
|
|||
public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); |
|||
|
|||
private static readonly AttachedProperty<NativeMenuInfo> s_nativeMenuInfoProperty = |
|||
AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, NativeMenuInfo>("___NativeMenuInfo"); |
|||
|
|||
class NativeMenuInfo |
|||
{ |
|||
public bool ChangingIsExported { get; set; } |
|||
public ITopLevelNativeMenuExporter Exporter { get; } |
|||
|
|||
public NativeMenuInfo(TopLevel target) |
|||
{ |
|||
Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; |
|||
if (Exporter != null) |
|||
{ |
|||
Exporter.OnIsNativeMenuExportedChanged += delegate |
|||
{ |
|||
SetIsNativeMenuExported(target, Exporter.IsNativeMenuExported); |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
|
|||
static NativeMenuInfo GetInfo(TopLevel target) |
|||
{ |
|||
var rv = target.GetValue(s_nativeMenuInfoProperty); |
|||
if (rv == null) |
|||
{ |
|||
target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target)); |
|||
SetIsNativeMenuExported(target, rv.Exporter.IsNativeMenuExported); |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
static void SetIsNativeMenuExported(TopLevel tl, bool value) |
|||
{ |
|||
GetInfo(tl).ChangingIsExported = true; |
|||
tl.SetValue(IsNativeMenuExportedProperty, value); |
|||
} |
|||
|
|||
public static readonly AttachedProperty<NativeMenu> MenuProperty |
|||
= AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("NativeMenuItems", validate: |
|||
(o, v) => |
|||
{ |
|||
if(!(o is Application || o is TopLevel)) |
|||
throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); |
|||
return v; |
|||
}); |
|||
|
|||
public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); |
|||
public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); |
|||
|
|||
|
|||
public static readonly AttachedProperty<bool> PrependApplicationMenuProperty |
|||
= AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, Boolean>("PrependApplicationMenu"); |
|||
|
|||
public static void SetPrependApplicationMenu(TopLevel tl, bool value) => |
|||
tl.SetValue(PrependApplicationMenuProperty, value); |
|||
|
|||
public static bool GetPrependApplicationMenu(TopLevel tl) => tl.GetValue(PrependApplicationMenuProperty); |
|||
|
|||
static NativeMenu() |
|||
{ |
|||
// This is needed because of the lack of attached direct properties
|
|||
IsNativeMenuExportedProperty.Changed.Subscribe(args => |
|||
{ |
|||
var info = GetInfo((TopLevel)args.Sender); |
|||
if (!info.ChangingIsExported) |
|||
throw new InvalidOperationException("IsNativeMenuExported property is read-only"); |
|||
info.ChangingIsExported = false; |
|||
}); |
|||
MenuProperty.Changed.Subscribe(args => |
|||
{ |
|||
if (args.Sender is TopLevel tl) |
|||
{ |
|||
GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); |
|||
} |
|||
}); |
|||
|
|||
PrependApplicationMenuProperty.Changed.Subscribe(args => |
|||
{ |
|||
GetInfo((TopLevel)args.Sender).Exporter?.SetPrependApplicationMenu((bool)args.NewValue); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItem> |
|||
{ |
|||
private AvaloniaList<NativeMenuItem> _items = |
|||
new AvaloniaList<NativeMenuItem> { ResetBehavior = ResetBehavior.Remove }; |
|||
private NativeMenuItem _parent; |
|||
[Content] |
|||
public IList<NativeMenuItem> Items => _items; |
|||
|
|||
public NativeMenu() |
|||
{ |
|||
_items.Validate = Validator; |
|||
_items.CollectionChanged += ItemsChanged; |
|||
} |
|||
|
|||
private void Validator(NativeMenuItem obj) |
|||
{ |
|||
if (obj.Parent != null) |
|||
throw new InvalidOperationException("NativeMenuItem already has a parent"); |
|||
} |
|||
|
|||
private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if(e.OldItems!=null) |
|||
foreach (NativeMenuItem i in e.OldItems) |
|||
i.Parent = null; |
|||
if(e.NewItems!=null) |
|||
foreach (NativeMenuItem i in e.NewItems) |
|||
i.Parent = this; |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenu, NativeMenuItem> ParentProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenu, NativeMenuItem>("Parent", o => o.Parent, (o, v) => o.Parent = v); |
|||
|
|||
public NativeMenuItem Parent |
|||
{ |
|||
get => _parent; |
|||
set => SetAndRaise(ParentProperty, ref _parent, value); |
|||
} |
|||
|
|||
|
|||
public void Add(NativeMenuItem item) => _items.Add(item); |
|||
|
|||
public IEnumerator<NativeMenuItem> GetEnumerator() => _items.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Windows.Input; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Input; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class NativeMenuItem : AvaloniaObject |
|||
{ |
|||
private string _header; |
|||
private KeyGesture _gesture; |
|||
private bool _enabled = true; |
|||
private NativeMenu _menu; |
|||
private NativeMenu _parent; |
|||
|
|||
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs> |
|||
{ |
|||
private readonly NativeMenuItem _parent; |
|||
|
|||
public CanExecuteChangedSubscriber(NativeMenuItem parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
public void OnEvent(object sender, EventArgs e) |
|||
{ |
|||
_parent.CanExecuteChanged(); |
|||
} |
|||
} |
|||
|
|||
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; |
|||
|
|||
static NativeMenuItem() |
|||
{ |
|||
MenuProperty.Changed.Subscribe(args => |
|||
{ |
|||
var item = (NativeMenuItem)args.Sender; |
|||
var value = (NativeMenu)args.NewValue; |
|||
if (value.Parent != null && value.Parent != item) |
|||
throw new InvalidOperationException("NativeMenu already has a parent"); |
|||
value.Parent = item; |
|||
}); |
|||
} |
|||
|
|||
public NativeMenuItem() |
|||
{ |
|||
_canExecuteChangedSubscriber = new CanExecuteChangedSubscriber(this); |
|||
} |
|||
|
|||
public NativeMenuItem(string header) : this() |
|||
{ |
|||
Header = header; |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, string> HeaderProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o._header, (o, v) => o._header = v); |
|||
|
|||
public string Header |
|||
{ |
|||
get => GetValue(HeaderProperty); |
|||
set => SetValue(HeaderProperty, value); |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o._gesture, (o,v)=> o._gesture = v); |
|||
|
|||
public KeyGesture Gesture |
|||
{ |
|||
get => GetValue(GestureProperty); |
|||
set => SetValue(GestureProperty, value); |
|||
} |
|||
|
|||
private ICommand _command; |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, ICommand> CommandProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, ICommand>(nameof(Command), |
|||
o => o._command, (o, v) => |
|||
{ |
|||
if (o._command != null) |
|||
WeakSubscriptionManager.Unsubscribe(o._command, |
|||
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); |
|||
o._command = v; |
|||
if (o._command != null) |
|||
WeakSubscriptionManager.Subscribe(o._command, |
|||
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); |
|||
o.CanExecuteChanged(); |
|||
}); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommandParameter"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<object> CommandParameterProperty = |
|||
Button.CommandParameterProperty.AddOwner<MenuItem>(); |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, bool> EnabledProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(Enabled), o => o._enabled, |
|||
(o, v) => o._enabled = v, true); |
|||
|
|||
public bool Enabled |
|||
{ |
|||
get => GetValue(EnabledProperty); |
|||
set => SetValue(EnabledProperty, value); |
|||
} |
|||
|
|||
void CanExecuteChanged() |
|||
{ |
|||
Enabled = _command?.CanExecute(null) ?? true; |
|||
} |
|||
|
|||
public ICommand Command |
|||
{ |
|||
get => GetValue(CommandProperty); |
|||
set => SetValue(CommandProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
|
|||
/// <see cref="NativeMenuItem"/>.
|
|||
/// </summary>
|
|||
public object CommandParameter |
|||
{ |
|||
get { return GetValue(CommandParameterProperty); } |
|||
set { SetValue(CommandParameterProperty, value); } |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o._menu, |
|||
(o, v) => |
|||
{ |
|||
if (v.Parent != null && v.Parent != o) |
|||
throw new InvalidOperationException("NativeMenu already has a parent"); |
|||
o._menu = v; |
|||
}); |
|||
|
|||
public NativeMenu Menu |
|||
{ |
|||
get => _menu; |
|||
set |
|||
{ |
|||
if (value.Parent != null && value.Parent != this) |
|||
throw new InvalidOperationException("NativeMenu already has a parent"); |
|||
SetAndRaise(MenuProperty, ref _menu, value); |
|||
} |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, NativeMenu> ParentProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>("Parent", o => o.Parent, (o, v) => o.Parent = v); |
|||
|
|||
public NativeMenu Parent |
|||
{ |
|||
get => _parent; |
|||
set => SetAndRaise(ParentProperty, ref _parent, value); |
|||
} |
|||
|
|||
|
|||
public event EventHandler Clicked; |
|||
|
|||
public void RaiseClick() |
|||
{ |
|||
Clicked?.Invoke(this, new EventArgs()); |
|||
|
|||
if (Command?.CanExecute(CommandParameter) == true) |
|||
{ |
|||
Command.Execute(CommandParameter); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
public interface ITopLevelNativeMenuExporter |
|||
{ |
|||
bool IsNativeMenuExported { get; } |
|||
event EventHandler OnIsNativeMenuExportedChanged; |
|||
void SetNativeMenu(NativeMenu menu); |
|||
void SetPrependApplicationMenu(bool prepend); |
|||
} |
|||
|
|||
public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl |
|||
{ |
|||
ITopLevelNativeMenuExporter NativeMenuExporter { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Threading; |
|||
using Tmds.DBus; |
|||
|
|||
namespace Avalonia.FreeDesktop |
|||
{ |
|||
public class DBusHelper |
|||
{ |
|||
/// <summary>
|
|||
/// This class uses synchronous execution at DBus connection establishment stage
|
|||
/// then switches to using AvaloniaSynchronizationContext
|
|||
/// </summary>
|
|||
class DBusSyncContext : SynchronizationContext |
|||
{ |
|||
private SynchronizationContext _ctx; |
|||
private object _lock = new object(); |
|||
|
|||
public override void Post(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_ctx != null) |
|||
_ctx?.Post(d, state); |
|||
else |
|||
lock (_lock) |
|||
d(state); |
|||
} |
|||
} |
|||
|
|||
public override void Send(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_ctx != null) |
|||
_ctx?.Send(d, state); |
|||
else |
|||
|
|||
d(state); |
|||
} |
|||
} |
|||
|
|||
public void Initialized() |
|||
{ |
|||
lock (_lock) |
|||
_ctx = new AvaloniaSynchronizationContext(); |
|||
} |
|||
} |
|||
public static Connection Connection { get; private set; } |
|||
|
|||
public static Exception TryInitialize(string dbusAddress = null) |
|||
{ |
|||
|
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
AvaloniaSynchronizationContext.InstallIfNeeded(); |
|||
var oldContext = SynchronizationContext.Current; |
|||
try |
|||
{ |
|||
|
|||
var dbusContext = new DBusSyncContext(); |
|||
SynchronizationContext.SetSynchronizationContext(dbusContext); |
|||
var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) |
|||
{ |
|||
AutoConnect = false, |
|||
SynchronizationContext = dbusContext |
|||
}); |
|||
// Connect synchronously
|
|||
conn.ConnectAsync().Wait(); |
|||
|
|||
// Initialize a brand new sync-context
|
|||
dbusContext.Initialized(); |
|||
Connection = conn; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
return e; |
|||
} |
|||
finally |
|||
{ |
|||
SynchronizationContext.SetSynchronizationContext(oldContext); |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
|
|||
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.DBusMenu |
|||
{ |
|||
|
|||
[DBusInterface("org.freedesktop.DBus.Properties")] |
|||
interface IFreeDesktopDBusProperties : IDBusObject |
|||
{ |
|||
Task<object> GetAsync(string prop); |
|||
Task<DBusMenuProperties> GetAllAsync(); |
|||
Task SetAsync(string prop, object val); |
|||
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler); |
|||
} |
|||
|
|||
[DBusInterface("com.canonical.dbusmenu")] |
|||
interface IDBusMenu : IFreeDesktopDBusProperties |
|||
{ |
|||
Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); |
|||
Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); |
|||
Task<object> GetPropertyAsync(int Id, string Name); |
|||
Task EventAsync(int Id, string EventId, object Data, uint Timestamp); |
|||
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); |
|||
Task<bool> AboutToShowAsync(int Id); |
|||
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); |
|||
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError = null); |
|||
} |
|||
|
|||
[Dictionary] |
|||
class DBusMenuProperties |
|||
{ |
|||
public uint Version { get; set; } = default (uint); |
|||
public string TextDirection { get; set; } = default (string); |
|||
public string Status { get; set; } = default (string); |
|||
public string[] IconThemePath { get; set; } = default (string[]); |
|||
} |
|||
|
|||
|
|||
[DBusInterface("com.canonical.AppMenu.Registrar")] |
|||
interface IRegistrar : IDBusObject |
|||
{ |
|||
Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); |
|||
Task UnregisterWindowAsync(uint WindowId); |
|||
Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); |
|||
Task<(uint, string, ObjectPath)[]> GetMenusAsync(); |
|||
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception> onError = null); |
|||
} |
|||
} |
|||
@ -0,0 +1,364 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
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, NativeMenuItem> _idsToItems = new Dictionary<int, NativeMenuItem>(); |
|||
private Dictionary<NativeMenuItem, int> _itemsToIds = new Dictionary<NativeMenuItem, int>(); |
|||
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; } |
|||
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; |
|||
if (i.Menu != null) |
|||
((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; |
|||
} |
|||
_idsToItems.Clear(); |
|||
_itemsToIds.Clear(); |
|||
LayoutUpdated?.Invoke((_revision++, 0)); |
|||
} |
|||
|
|||
void QueueReset() |
|||
{ |
|||
if(_resetQueued) |
|||
return; |
|||
_resetQueued = true; |
|||
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); |
|||
} |
|||
|
|||
private (NativeMenuItem item, NativeMenu menu) GetMenu(int id) |
|||
{ |
|||
if (id == 0) |
|||
return (null, _menu); |
|||
_idsToItems.TryGetValue(id, out var item); |
|||
return (item, item?.Menu); |
|||
} |
|||
|
|||
private int GetId(NativeMenuItem item) |
|||
{ |
|||
if (_itemsToIds.TryGetValue(item, out var id)) |
|||
return id; |
|||
id = _nextId++; |
|||
_idsToItems[id] = item; |
|||
_itemsToIds[item] = id; |
|||
item.PropertyChanged += OnItemPropertyChanged; |
|||
if (item.Menu != null) |
|||
((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; |
|||
return id; |
|||
} |
|||
|
|||
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
public void SetPrependApplicationMenu(bool prepend) |
|||
{ |
|||
// Not implemented yet :(
|
|||
} |
|||
|
|||
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" |
|||
}; |
|||
|
|||
object GetProperty((NativeMenuItem item, NativeMenu menu) i, string name) |
|||
{ |
|||
var (item, menu) = i; |
|||
if (name == "type") |
|||
{ |
|||
if (item != null && item.Header == null) |
|||
return "separator"; |
|||
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.Enabled == 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 == "children-display") |
|||
return menu != null ? "submenu" : null; |
|||
return null; |
|||
} |
|||
|
|||
private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>(); |
|||
KeyValuePair<string, object>[] GetProperties((NativeMenuItem 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)); |
|||
return Task.FromResult(rv); |
|||
} |
|||
|
|||
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItem 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.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?.Enabled == true) |
|||
item.RaiseClick(); |
|||
} |
|||
} |
|||
|
|||
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
|
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue