NativeMenu/NativeMenu item with dbusmenu-based exporter
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=3E53A01A_002DB331_002D47F3_002DB828_002D4A5717E77A24_002Fd_003Aglass/@EntryIndexedValue">ExplicitlyExcluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6417B24E_002D49C2_002D4985_002D8DB2_002D3AB9D898EC91/@EntryIndexedValue">ExplicitlyExcluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=E3A1060B_002D50D0_002D44E8_002D88B6_002DF44EF2E5BD72_002Ff_003Ahtml_002Ehtm/@EntryIndexedValue">ExplicitlyExcluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=DECLSPEC_005FPROPERTY/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=ENUM/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
|
||||
@@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.With(new X11PlatformOptions { EnableMultiTouch = true })
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
EnableMultiTouch = true,
|
||||
UseDBusMenu = true
|
||||
})
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
EnableMultitouch = true,
|
||||
|
||||
@@ -8,7 +8,34 @@
|
||||
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
|
||||
xmlns:v="clr-namespace:ControlCatalog.Views"
|
||||
x:Class="ControlCatalog.MainWindow">
|
||||
<Window.DataTemplates>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="File">
|
||||
<NativeMenuItem.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
|
||||
<NativeMenuItem Header="Recent">
|
||||
<NativeMenuItem.Menu>
|
||||
<NativeMenu/>
|
||||
</NativeMenuItem.Menu>
|
||||
</NativeMenuItem>
|
||||
<NativeMenuItem Header="Close" Clicked="OnCloseClicked"/>
|
||||
</NativeMenu>
|
||||
</NativeMenuItem.Menu>
|
||||
</NativeMenuItem>
|
||||
<NativeMenuItem Header="Edit">
|
||||
<NativeMenuItem.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="Copy"/>
|
||||
<NativeMenuItem Header="Paste"/>
|
||||
</NativeMenu>
|
||||
</NativeMenuItem.Menu>
|
||||
</NativeMenuItem>
|
||||
</NativeMenu>
|
||||
</NativeMenu.Menu>
|
||||
|
||||
<Window.DataTemplates>
|
||||
<DataTemplate DataType="vm:NotificationViewModel">
|
||||
<v:CustomNotificationView />
|
||||
</DataTemplate>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ControlCatalog
|
||||
public class MainWindow : Window
|
||||
{
|
||||
private WindowNotificationManager _notificationArea;
|
||||
private NativeMenu _recentMenu;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -29,8 +30,21 @@ namespace ControlCatalog
|
||||
};
|
||||
|
||||
DataContext = new MainWindowViewModel(_notificationArea);
|
||||
_recentMenu = NativeMenu.GetMenu(this).Items[0].Menu.Items[1].Menu;
|
||||
}
|
||||
|
||||
public void OnOpenClicked(object sender, EventArgs args)
|
||||
{
|
||||
_recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
|
||||
}
|
||||
|
||||
public void OnCloseClicked(object sender, EventArgs args)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
// TODO: iOS does not support dynamically loading assemblies
|
||||
|
||||
@@ -210,5 +210,22 @@ namespace Avalonia
|
||||
{
|
||||
ResourcesChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
/// <summary>
|
||||
/// Defines Name property
|
||||
/// </summary>
|
||||
public static readonly DirectProperty<Application, string> NameProperty =
|
||||
AvaloniaProperty.RegisterDirect<Application, string>("Name", o => o.Name, (o, v) => o.Name = v);
|
||||
|
||||
/// <summary>
|
||||
/// Application name to be used for various platform-specific purposes
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetAndRaise(NameProperty, ref _name, value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,8 @@
|
||||
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Tmds.DBus" Version="0.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ namespace Avalonia.X11
|
||||
throw new Exception("XOpenDisplay failed");
|
||||
XError.Init();
|
||||
Info = new X11Info(Display, DeferredDisplay);
|
||||
|
||||
//TODO: log
|
||||
if (options.UseDBusMenu)
|
||||
DBusHelper.TryInitialize();
|
||||
AvaloniaLocator.CurrentMutable.BindToSelf(this)
|
||||
.Bind<IWindowingPlatform>().ToConstant(this)
|
||||
.Bind<IPlatformThreadingInterface>().ToConstant(new X11PlatformThreading(this))
|
||||
@@ -95,6 +97,7 @@ namespace Avalonia
|
||||
public bool UseEGL { get; set; }
|
||||
public bool UseGpu { get; set; } = true;
|
||||
public bool OverlayPopups { get; set; }
|
||||
public bool UseDBusMenu { get; set; }
|
||||
|
||||
public List<string> GlxRendererBlacklist { get; set; } = new List<string>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,9 @@ using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Text;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Platform;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.FreeDesktop;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Raw;
|
||||
using Avalonia.OpenGL;
|
||||
@@ -19,7 +21,7 @@ using static Avalonia.X11.XLib;
|
||||
// ReSharper disable StringLiteralTypo
|
||||
namespace Avalonia.X11
|
||||
{
|
||||
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client
|
||||
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter
|
||||
{
|
||||
private readonly AvaloniaX11Platform _platform;
|
||||
private readonly IWindowImpl _popupParent;
|
||||
@@ -155,6 +157,8 @@ namespace Avalonia.X11
|
||||
XFlush(_x11.Display);
|
||||
if(_popup)
|
||||
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
|
||||
if (platform.Options.UseDBusMenu)
|
||||
NativeMenuExporter = DBusMenuExporter.TryCreate(_handle);
|
||||
}
|
||||
|
||||
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
|
||||
@@ -960,5 +964,6 @@ namespace Avalonia.X11
|
||||
}
|
||||
|
||||
public IPopupPositioner PopupPositioner { get; }
|
||||
public ITopLevelNativeMenuExporter NativeMenuExporter { get; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user