Browse Source

NativeMenu/NativeMenu item with dbusmenu-based exporter

Nikita Tsukanov 6 years ago
parent
commit
19962e4c4d

+ 1 - 0
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>

+ 5 - 1
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,

+ 28 - 1
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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
+        }
+    }
+}

+ 4 - 1
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>
         {

+ 6 - 1
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; }
     }
 }