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