Browse Source

NativeMenu/NativeMenu item with dbusmenu-based exporter

pull/2978/head
Nikita Tsukanov 7 years ago
parent
commit
19962e4c4d
  1. 1
      Avalonia.sln.DotSettings
  2. 6
      samples/ControlCatalog.NetCore/Program.cs
  3. 29
      samples/ControlCatalog/MainWindow.xaml
  4. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  5. 17
      src/Avalonia.Controls/Application.cs
  6. 100
      src/Avalonia.Controls/NativeMenu.Export.cs
  7. 61
      src/Avalonia.Controls/NativeMenu.cs
  8. 171
      src/Avalonia.Controls/NativeMenuItem.cs
  9. 19
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  10. 4
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  11. 85
      src/Avalonia.FreeDesktop/DBusHelper.cs
  12. 56
      src/Avalonia.FreeDesktop/DBusMenu.cs
  13. 364
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  14. 5
      src/Avalonia.X11/X11Platform.cs
  15. 7
      src/Avalonia.X11/X11Window.cs

1
Avalonia.sln.DotSettings

@ -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">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=ENUM/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>

6
samples/ControlCatalog.NetCore/Program.cs

@ -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,

29
samples/ControlCatalog/MainWindow.xaml

@ -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
samples/ControlCatalog/MainWindow.xaml.cs

@ -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

17
src/Avalonia.Controls/Application.cs

@ -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);
}
}
}

100
src/Avalonia.Controls/NativeMenu.Export.cs

@ -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);
});
}
}
}

61
src/Avalonia.Controls/NativeMenu.cs

@ -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();
}
}
}

171
src/Avalonia.Controls/NativeMenuItem.cs

@ -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);
}
}
}
}

19
src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs

@ -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; }
}
}

4
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -8,4 +8,8 @@
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Tmds.DBus" Version="0.7.0" />
</ItemGroup>
</Project>

85
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -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;
}
}
}

56
src/Avalonia.FreeDesktop/DBusMenu.cs

@ -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);
}
}

364
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -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
}
}
}

5
src/Avalonia.X11/X11Platform.cs

@ -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>
{

7
src/Avalonia.X11/X11Window.cs

@ -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; }
}
}

Loading…
Cancel
Save