From ef7e8f2107749bc629675512ac1daa97a0ecc00a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 Sep 2019 18:19:19 +0300 Subject: [PATCH 01/61] Make Application to be an AvaloniaObject. With properties and stuff. --- .../Avalonia.Android/AndroidPlatform.cs | 6 +-- src/Avalonia.Controls/AppBuilderBase.cs | 50 +++++++++++-------- src/Avalonia.Controls/Application.cs | 2 +- .../ClassicDesktopStyleApplicationLifetime.cs | 3 +- src/Avalonia.DesktopRuntime/AppBuilder.cs | 11 +--- .../LinuxFramebufferPlatform.cs | 3 +- src/iOS/Avalonia.iOS/AppBuilder.cs | 2 +- 7 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c91b58311b..c11cadfbac 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -17,7 +17,7 @@ namespace Avalonia { public static T UseAndroid(this T builder) where T : AppBuilderBase, new() { - builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.Instance), "Android"); + builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android"); builder.UseSkia(); return builder; } @@ -41,7 +41,7 @@ namespace Avalonia.Android _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; } - public static void Initialize(Avalonia.Application app) + public static void Initialize(Type appType) { AvaloniaLocator.CurrentMutable .Bind().ToTransient() @@ -55,7 +55,7 @@ namespace Avalonia.Android .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton() - .Bind().ToConstant(new AssetLoader(app.GetType().Assembly)); + .Bind().ToConstant(new AssetLoader(appType.Assembly)); SkiaPlatform.Initialize(); ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 307ddd284c..2f796376ea 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls { private static bool s_setupWasAlreadyCalled; private Action _optionsInitializers; - + private Func _appFactory; + private IApplicationLifetime _lifetime; + /// /// Gets or sets the instance. /// @@ -30,10 +32,15 @@ namespace Avalonia.Controls public Action RuntimePlatformServicesInitializer { get; private set; } /// - /// Gets or sets the instance being initialized. + /// Gets the instance being initialized. /// - public Application Instance { get; protected set; } - + public Application Instance { get; private set; } + + /// + /// Gets the type of the Instance (even if it's not created yet) + /// + public Type ApplicationType { get; private set; } + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -76,20 +83,11 @@ namespace Avalonia.Controls public static TAppBuilder Configure() where TApp : Application, new() { - return Configure(new TApp()); - } - - /// - /// Begin configuring an . - /// - /// An instance. - public static TAppBuilder Configure(Application app) - { - AvaloniaLocator.CurrentMutable.BindToSelf(app); - return new TAppBuilder() { - Instance = app, + ApplicationType = typeof(TApp), + // Needed for CoreRT compatibility + _appFactory = () => new TApp() }; } @@ -157,6 +155,18 @@ namespace Avalonia.Controls return Self; } + /// + /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it. + /// + /// + /// + public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime) + { + _lifetime = lifetime; + Setup(); + return Self; + } + /// /// Specifies a windowing subsystem to use. /// @@ -254,11 +264,6 @@ namespace Avalonia.Controls /// private void Setup() { - if (Instance == null) - { - throw new InvalidOperationException("No App instance configured."); - } - if (RuntimePlatformServicesInitializer == null) { throw new InvalidOperationException("No runtime platform services configured."); @@ -282,6 +287,9 @@ namespace Avalonia.Controls s_setupWasAlreadyCalled = true; _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer(); + Instance = _appFactory(); + Instance.ApplicationLifetime = _lifetime; + AvaloniaLocator.CurrentMutable.BindToSelf(Instance); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 382106de65..ba842d8825 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index abca7a64ee..2533191ae4 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -125,8 +125,7 @@ namespace Avalonia where T : AppBuilderBase, new() { var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } } diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs index dbe3767df6..ff0d84a6e9 100644 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ b/src/Avalonia.DesktopRuntime/AppBuilder.cs @@ -18,19 +18,10 @@ namespace Avalonia /// public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { } - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public AppBuilder(Application app) : this() - { - Instance = app; - } - bool CheckEnvironment(Type checkerType) { if (checkerType == null) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8fc555aac2..db37e4af0b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -118,8 +118,7 @@ public static class LinuxFramebufferPlatformExtensions where T : AppBuilderBase, new() { var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); return lifetime.ExitCode; diff --git a/src/iOS/Avalonia.iOS/AppBuilder.cs b/src/iOS/Avalonia.iOS/AppBuilder.cs index a68dd6387a..cb8e0a7954 100644 --- a/src/iOS/Avalonia.iOS/AppBuilder.cs +++ b/src/iOS/Avalonia.iOS/AppBuilder.cs @@ -6,7 +6,7 @@ namespace Avalonia public class AppBuilder : AppBuilderBase { public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType().Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { } From 19962e4c4dad850aec3208a5b2b8d72fba583ffe Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 Sep 2019 22:41:12 +0300 Subject: [PATCH 02/61] NativeMenu/NativeMenu item with dbusmenu-based exporter --- Avalonia.sln.DotSettings | 1 + samples/ControlCatalog.NetCore/Program.cs | 6 +- samples/ControlCatalog/MainWindow.xaml | 29 +- samples/ControlCatalog/MainWindow.xaml.cs | 14 + src/Avalonia.Controls/Application.cs | 17 + src/Avalonia.Controls/NativeMenu.Export.cs | 100 +++++ src/Avalonia.Controls/NativeMenu.cs | 61 +++ src/Avalonia.Controls/NativeMenuItem.cs | 171 ++++++++ .../Platform/ITopLevelNativeMenuExporter.cs | 19 + .../Avalonia.FreeDesktop.csproj | 4 + src/Avalonia.FreeDesktop/DBusHelper.cs | 85 ++++ src/Avalonia.FreeDesktop/DBusMenu.cs | 56 +++ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 364 ++++++++++++++++++ src/Avalonia.X11/X11Platform.cs | 5 +- src/Avalonia.X11/X11Window.cs | 7 +- 15 files changed, 935 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Controls/NativeMenu.Export.cs create mode 100644 src/Avalonia.Controls/NativeMenu.cs create mode 100644 src/Avalonia.Controls/NativeMenuItem.cs create mode 100644 src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs create mode 100644 src/Avalonia.FreeDesktop/DBusHelper.cs create mode 100644 src/Avalonia.FreeDesktop/DBusMenu.cs create mode 100644 src/Avalonia.FreeDesktop/DBusMenuExporter.cs diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 1361172fff..7060f4a62a 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -3,6 +3,7 @@ ExplicitlyExcluded ExplicitlyExcluded ExplicitlyExcluded + DO_NOT_SHOW HINT <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d683092edf..854cae484c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions { EnableMultiTouch = true }) + .With(new X11PlatformOptions + { + EnableMultiTouch = true, + UseDBusMenu = true + }) .With(new Win32PlatformOptions { EnableMultitouch = true, diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 9527ac3b4e..fca155ef70 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,34 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" x:Class="ControlCatalog.MainWindow"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 95c65ed92f..9f62c0da38 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/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 diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ba842d8825..ce60a0f0b9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -210,5 +210,22 @@ namespace Avalonia { ResourcesChanged?.Invoke(this, e); } + + private string _name; + /// + /// Defines Name property + /// + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + + /// + /// Application name to be used for various platform-specific purposes + /// + public string Name + { + get => _name; + set => SetAndRaise(NameProperty, ref _name, value); + } + } } diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs new file mode 100644 index 0000000000..9ce83f3e3c --- /dev/null +++ b/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 IsNativeMenuExportedProperty = + AvaloniaProperty.RegisterAttached("IsNativeMenuExported", + defaultBindingMode: BindingMode.OneWayToSource); + + public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); + + private static readonly AttachedProperty s_nativeMenuInfoProperty = + AvaloniaProperty.RegisterAttached("___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 MenuProperty + = AvaloniaProperty.RegisterAttached("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 PrependApplicationMenuProperty + = AvaloniaProperty.RegisterAttached("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); + }); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs new file mode 100644 index 0000000000..1e2966ff2b --- /dev/null +++ b/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 + { + private AvaloniaList _items = + new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; + private NativeMenuItem _parent; + [Content] + public IList 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 ParentProperty = + AvaloniaProperty.RegisterDirect("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 GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs new file mode 100644 index 0000000000..3f1a80dcfe --- /dev/null +++ b/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 + { + 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 HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); + + public string Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public static readonly DirectProperty GestureProperty = + AvaloniaProperty.RegisterDirect(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 CommandProperty = + AvaloniaProperty.RegisterDirect(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(); + }); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + public static readonly DirectProperty EnabledProperty = + AvaloniaProperty.RegisterDirect(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); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public static readonly DirectProperty MenuProperty = + AvaloniaProperty.RegisterDirect(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 ParentProperty = + AvaloniaProperty.RegisterDirect("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); + } + } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs new file mode 100644 index 0000000000..5112424c3c --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index d7e1d8cdb3..d9fd3b78a2 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs new file mode 100644 index 0000000000..509e312e1b --- /dev/null +++ b/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 + { + /// + /// This class uses synchronous execution at DBus connection establishment stage + /// then switches to using AvaloniaSynchronizationContext + /// + 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; + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs new file mode 100644 index 0000000000..7180345386 --- /dev/null +++ b/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 GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [DBusInterface("com.canonical.dbusmenu")] + interface IDBusMenu : IFreeDesktopDBusProperties + { + Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); + Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); + Task GetPropertyAsync(int Id, string Name); + Task EventAsync(int Id, string EventId, object Data, uint Timestamp); + Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); + Task AboutToShowAsync(int Id); + Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action 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 WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs new file mode 100644 index 0000000000..d9b37607f6 --- /dev/null +++ b/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 _idsToItems = new Dictionary(); + private Dictionary _itemsToIds = new Dictionary(); + 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( + "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 IFreeDesktopDBusProperties.GetAsync(string prop) + { + if (prop == "Version") + return 2; + if (prop == "Status") + return "normal"; + return 0; + } + + async Task 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 ?? ""; + 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(); + 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> _reusablePropertyList = new List>(); + KeyValuePair[] 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(n, v)); + } + + return _reusablePropertyList.ToArray(); + } + + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task<(uint revision, (int, KeyValuePair[], 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[], 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[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) + { + var arr = new (int, KeyValuePair[])[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 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 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 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)[] updatedProps, (int, string[])[] removedProps)> + ItemsPropertiesUpdated; + private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(int id, uint timestamp)> ItemActivationRequested; + private event Action PropertiesChanged; + + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + { + ItemsPropertiesUpdated += handler; + return Disposable.Create(() => ItemsPropertiesUpdated -= handler); + } + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + { + LayoutUpdated += handler; + return Disposable.Create(() => LayoutUpdated -= handler); + } + + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + { + ItemActivationRequested+= handler; + return Disposable.Create(() => ItemActivationRequested -= handler); + } + + async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) + { + PropertiesChanged += handler; + return Disposable.Create(() => PropertiesChanged -= handler); + } + + #endregion + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1d2290236c..d7a7bb97fd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/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().ToConstant(this) .Bind().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 GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 975b3d11d7..0fab85d681 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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; } } } From a25b39399e0460dd2199549b0070e354b7c796a4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 Sep 2019 22:46:43 +0300 Subject: [PATCH 03/61] Report menu export status change --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index d9b37607f6..18ba4a90d4 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -78,7 +78,7 @@ namespace Avalonia.FreeDesktop - public bool IsNativeMenuExported { get; } + public bool IsNativeMenuExported { get; private set; } public event EventHandler OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu menu) @@ -255,6 +255,14 @@ namespace Avalonia.FreeDesktop { var menu = GetMenu(ParentId); var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => + { + OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); + }); + } return Task.FromResult(rv); } From 2e643d65a99997103f04400a60959dcb1c113b91 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 Sep 2019 23:06:03 +0300 Subject: [PATCH 04/61] Fixed threading --- src/Avalonia.Controls/AppBuilderBase.cs | 6 +++--- src/Avalonia.FreeDesktop/DBusHelper.cs | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 2f796376ea..d9be9171ed 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -287,12 +287,12 @@ namespace Avalonia.Controls s_setupWasAlreadyCalled = true; _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer(); - Instance = _appFactory(); - Instance.ApplicationLifetime = _lifetime; - AvaloniaLocator.CurrentMutable.BindToSelf(Instance); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); + Instance = _appFactory(); + Instance.ApplicationLifetime = _lifetime; + AvaloniaLocator.CurrentMutable.BindToSelf(Instance); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 509e312e1b..b445f86613 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -50,9 +50,6 @@ namespace Avalonia.FreeDesktop public static Exception TryInitialize(string dbusAddress = null) { - - Dispatcher.UIThread.VerifyAccess(); - AvaloniaSynchronizationContext.InstallIfNeeded(); var oldContext = SynchronizationContext.Current; try { From 641c07a5c0b0efdbbf248d67837d88bbd60b5b31 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 Sep 2019 23:34:54 +0300 Subject: [PATCH 05/61] [DBusMenu] Send proper _revision value to LayoutUpdated --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 18ba4a90d4..1f1a244fb7 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -112,7 +112,8 @@ namespace Avalonia.FreeDesktop } _idsToItems.Clear(); _itemsToIds.Clear(); - LayoutUpdated?.Invoke((_revision++, 0)); + _revision++; + LayoutUpdated?.Invoke((_revision, 0)); } void QueueReset() From 8520eb32bc164b6ad648da7daeebd6c213597df0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 22 Sep 2019 11:07:43 +0100 Subject: [PATCH 06/61] add scafolding for native osx menu --- .../AvaloniaNativeMenuExporter.cs | 25 +++++++++++++++++++ src/Avalonia.Native/WindowImpl.cs | 8 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Native/AvaloniaNativeMenuExporter.cs diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs new file mode 100644 index 0000000000..76598237e2 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Platform; + +namespace Avalonia.Native +{ + class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + { + public bool IsNativeMenuExported => throw new NotImplementedException(); + + public event EventHandler OnIsNativeMenuExportedChanged; + + public void SetNativeMenu(NativeMenu menu) + { + throw new NotImplementedException(); + } + + public void SetPrependApplicationMenu(bool prepend) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 490d5688a8..76ee52f3ab 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -3,13 +3,14 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Native.Interop; using Avalonia.Platform; using Avalonia.Platform.Interop; namespace Avalonia.Native { - public class WindowImpl : WindowBaseImpl, IWindowImpl + public class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; @@ -22,6 +23,8 @@ namespace Avalonia.Native { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); } + + NativeMenuExporter = new AvaloniaNativeMenuExporter(); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -104,6 +107,9 @@ namespace Avalonia.Native } public Func Closing { get; set; } + + public ITopLevelNativeMenuExporter NativeMenuExporter { get; } + public void Move(PixelPoint point) => Position = point; public override IPopupImpl CreatePopup() => From 72c950588afedd3827858f58ed6bd74754cea97a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 22 Sep 2019 11:49:23 +0100 Subject: [PATCH 07/61] add some more menu implementation. --- .../AvaloniaNativeMenuExporter.cs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 76598237e2..59a92a0ba8 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,25 +1,86 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Threading; namespace Avalonia.Native { class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { + private NativeMenu _menu; + private bool _resetQueued; + private Dictionary _idsToItems = new Dictionary(); + private Dictionary _itemsToIds = new Dictionary(); + private uint _revision = 1; + public bool IsNativeMenuExported => throw new NotImplementedException(); public event EventHandler OnIsNativeMenuExportedChanged; + private event Action<(uint revision, int parent)> LayoutUpdated; + public void SetNativeMenu(NativeMenu menu) { - throw new NotImplementedException(); + if (menu == null) + menu = new NativeMenu(); + + if (_menu != null) + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menu = menu; + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + + DoLayoutReset(); } public void SetPrependApplicationMenu(bool prepend) { throw new NotImplementedException(); } + + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + QueueReset(); + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + QueueReset(); + } + + /* + 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(); + + _revision++; + + LayoutUpdated?.Invoke((_revision, 0)); + } + + private void QueueReset() + { + if (_resetQueued) + return; + _resetQueued = true; + Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); + } } } From 160deba7725fbb4b5fb37600064633d1d90bd17b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 15:27:47 +0100 Subject: [PATCH 08/61] fix null check when there is no exporter. --- src/Avalonia.Controls/NativeMenu.Export.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 9ce83f3e3c..40ea8bd2d4 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls if (rv == null) { target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target)); - SetIsNativeMenuExported(target, rv.Exporter.IsNativeMenuExported); + SetIsNativeMenuExported(target, rv.Exporter?.IsNativeMenuExported ?? false); } return rv; From 22e14443ab2f3492183438262b2e5482b79151dd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 15:37:19 +0100 Subject: [PATCH 09/61] add native apis for setting working with osx menus --- native/Avalonia.Native/inc/IGetNative.h | 10 + native/Avalonia.Native/inc/avalonia-native.h | 27 +++ .../project.pbxproj | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + native/Avalonia.Native/src/OSX/common.h | 9 + native/Avalonia.Native/src/OSX/main.mm | 127 ++++++++++- native/Avalonia.Native/src/OSX/menu.h | 79 +++++++ native/Avalonia.Native/src/OSX/menu.mm | 211 ++++++++++++++++++ .../src/OSX/platformthreading.mm | 6 - 9 files changed, 474 insertions(+), 10 deletions(-) create mode 100644 native/Avalonia.Native/inc/IGetNative.h create mode 100644 native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 native/Avalonia.Native/src/OSX/menu.h create mode 100644 native/Avalonia.Native/src/OSX/menu.mm diff --git a/native/Avalonia.Native/inc/IGetNative.h b/native/Avalonia.Native/inc/IGetNative.h new file mode 100644 index 0000000000..85ae030d74 --- /dev/null +++ b/native/Avalonia.Native/inc/IGetNative.h @@ -0,0 +1,10 @@ +#ifndef igetnative_h +#define igetnative_h + +class IGetNative +{ +public: + virtual void* GetNative() = 0; +}; + +#endif diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index e54f3fa6a7..16d4fa4107 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -22,6 +22,8 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; +struct IAvnAppMenu; +struct IAvnAppMenuItem; struct AvnSize { @@ -173,6 +175,9 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainAppMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -258,6 +263,7 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents AVNCOM(IAvnMacOptions, 07) : IUnknown { virtual HRESULT SetShowInDock(int show) = 0; + virtual HRESULT SetApplicationTitle (void* utf8string) = 0; }; AVNCOM(IAvnActionCallback, 08) : IUnknown @@ -367,4 +373,25 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; +AVNCOM(IAvnAppMenu, 17) : IUnknown +{ + virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT Clear () = 0; +}; + +AVNCOM(IAvnPredicateCallback, 18) : IUnknown +{ + virtual bool Evaluate() = 0; +}; + +AVNCOM(IAvnAppMenuItem, 19) : IUnknown +{ + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 1870ef7ab3..84c3a84b91 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; + 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -32,6 +33,8 @@ 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; + 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; + 5296D43022F30EBC005B125D /* menu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = menu.h; path = ../../../../../../../../System/Volumes/Data/Users/danwalmsley/repos/Avalonia/native/Avalonia.Native/src/OSX/menu.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -85,6 +88,8 @@ AB661C1F2148286E00291242 /* window.mm */, 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, + 520624B222973F4100C4DCEF /* menu.mm */, + 5296D43022F30EBC005B125D /* menu.h */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -150,6 +155,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = AB7A61E62147C814003C5833; @@ -173,6 +179,7 @@ 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, + 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 45ec40c361..c066ebb498 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,6 +19,10 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); +extern IAvnAppMenu* GetAppMenu(); +extern IAvnAppMenu* CreateAppMenu(); +extern IAvnAppMenuItem* CreateAppMenuItem(); + extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); @@ -40,4 +44,9 @@ template inline T* objc_cast(id from) { return nil; } +@interface ActionCallback : NSObject +- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; +- (void) action; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 70bd1e67f6..159f01d1d7 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,10 +5,115 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +void SetProcessName(CFStringRef process_name) { + if (!process_name || CFStringGetLength(process_name) == 0) { + //NOTREACHED() << "SetProcessName given bad name."; + return; + } + + if (![NSThread isMainThread]) { + //NOTREACHED() << "Should only set process name from main thread."; + return; + } + + // Warning: here be dragons! This is SPI reverse-engineered from WebKit's + // plugin host, and could break at any time (although realistically it's only + // likely to break in a new major release). + // When 10.7 is available, check that this still works, and update this + // comment for 10.8. + + // Private CFType used in these LaunchServices calls. + typedef CFTypeRef PrivateLSASN; + typedef PrivateLSASN (*LSGetCurrentApplicationASNType)(); + typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN, + CFStringRef, + CFStringRef, + CFDictionaryRef*); + + static LSGetCurrentApplicationASNType ls_get_current_application_asn_func = + NULL; + static LSSetApplicationInformationItemType + ls_set_application_information_item_func = NULL; + static CFStringRef ls_display_name_key = NULL; + + static bool did_symbol_lookup = false; + if (!did_symbol_lookup) { + did_symbol_lookup = true; + CFBundleRef launch_services_bundle = + CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices")); + if (!launch_services_bundle) { + //LOG(ERROR) << "Failed to look up LaunchServices bundle"; + return; + } + + ls_get_current_application_asn_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN"))); + if (!ls_get_current_application_asn_func){} + //LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN"; + + ls_set_application_information_item_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, + CFSTR("_LSSetApplicationInformationItem"))); + if (!ls_set_application_information_item_func){} + //LOG(ERROR) << "Could not find _LSSetApplicationInformationItem"; + + CFStringRef* key_pointer = reinterpret_cast( + CFBundleGetDataPointerForName(launch_services_bundle, + CFSTR("_kLSDisplayNameKey"))); + ls_display_name_key = key_pointer ? *key_pointer : NULL; + if (!ls_display_name_key){} + //LOG(ERROR) << "Could not find _kLSDisplayNameKey"; + + // Internally, this call relies on the Mach ports that are started up by the + // Carbon Process Manager. In debug builds this usually happens due to how + // the logging layers are started up; but in release, it isn't started in as + // much of a defined order. So if the symbols had to be loaded, go ahead + // and force a call to make sure the manager has been initialized and hence + // the ports are opened. + ProcessSerialNumber psn; + GetCurrentProcess(&psn); + } + if (!ls_get_current_application_asn_func || + !ls_set_application_information_item_func || + !ls_display_name_key) { + return; + } + + PrivateLSASN asn = ls_get_current_application_asn_func(); + // Constant used by WebKit; what exactly it means is unknown. + const int magic_session_constant = -2; + OSErr err = + ls_set_application_information_item_func(magic_session_constant, asn, + ls_display_name_key, + process_name, + NULL /* optional out param */); + //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err; +} + class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() + + virtual HRESULT SetApplicationTitle(void* utf8String) override + { + auto appTitle = [NSString stringWithUTF8String:(const char*)utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + CFStringRef titleRef = (__bridge CFStringRef)appTitle; + SetProcessName(titleRef); + + return S_OK; + } + virtual HRESULT SetShowInDock(int show) override { AvnDesiredActivationPolicy = show @@ -17,8 +122,6 @@ public: } }; - - /// See "Using POSIX Threads in a Cocoa Application" section here: /// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024 @interface ThreadingInitializer : NSObject @@ -43,8 +146,6 @@ public: close(_fds[0]); close(_fds[1]); } - - @end @@ -123,6 +224,24 @@ public: *ppv = rv; return S_OK; } + + virtual HRESULT ObtainAppMenu(IAvnAppMenu** ppv) override + { + *ppv = ::GetAppMenu(); + return S_OK; + } + + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + { + *ppv = ::CreateAppMenu(); + return S_OK; + } + + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h new file mode 100644 index 0000000000..e3c1fa9768 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -0,0 +1,79 @@ +// +// menu.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 01/08/2019. +// Copyright © 2019 Avalonia. All rights reserved. +// + +#ifndef menu_h +#define menu_h + +#include "common.h" + +class AvnAppMenuItem; +class AvnAppMenu; + +@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain + +@end + +@interface AvnMenuItem : NSMenuItem +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem; +- (void)didSelectItem:(id)sender; +@end + +class AvnAppMenuItem : public ComSingleObject +{ +private: + AvnMenuItem* _native; // here we hold a pointer to an AvnMenuItem + IAvnActionCallback* _callback; + IAvnPredicateCallback* _predicate; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenuItem(); + + AvnMenuItem* GetNative(); + + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT SetGesture (void* key, AvnInputModifiers modifiers) override; + + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + + bool EvaluateItemEnabled(); + + void RaiseOnClicked(); +}; + + +class AvnAppMenu : public ComSingleObject +{ +private: + AvnMenu* _native; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenu(); + + AvnAppMenu(AvnMenu* native); + + AvnMenu* GetNative(); + + virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT Clear () override; +}; + + +#endif + diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm new file mode 100644 index 0000000000..f861774b29 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -0,0 +1,211 @@ + +#include "common.h" +#include "IGetNative.h" +#include "menu.h" + +@implementation AvnMenu +@end + +@implementation AvnMenuItem +{ + AvnAppMenuItem* _item; +} + +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem +{ + if(self != nil) + { + _item = menuItem; + self = [super initWithTitle:@"" + action:@selector(didSelectItem:) + keyEquivalent:@""]; + + [self setEnabled:YES]; + + [self setTarget:self]; + } + + return self; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + if([self submenu] != nil) + { + return YES; + } + + return _item->EvaluateItemEnabled(); +} + +- (void)didSelectItem:(nullable id)sender +{ + _item->RaiseOnClicked(); +} +@end + +AvnAppMenuItem::AvnAppMenuItem() +{ + _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + _callback = nullptr; +} + +AvnMenuItem* AvnAppMenuItem::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +{ + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetTitle (void* utf8String) +{ + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers) +{ + NSEventModifierFlags flags = 0; + + if (modifiers & Control) + flags |= NSEventModifierFlagControl; + if (modifiers & Shift) + flags |= NSEventModifierFlagShift; + if (modifiers & Alt) + flags |= NSEventModifierFlagOption; + if (modifiers & Windows) + flags |= NSEventModifierFlagCommand; + + [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; + [_native setKeyEquivalentModifierMask:flags]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) +{ + _predicate = predicate; + _callback = callback; + return S_OK; +} + +bool AvnAppMenuItem::EvaluateItemEnabled() +{ + if(_predicate != nullptr) + { + auto result = _predicate->Evaluate (); + + return result; + } + + return false; +} + +void AvnAppMenuItem::RaiseOnClicked() +{ + if(_callback != nullptr) + { + _callback->Run(); + } +} + +AvnAppMenu::AvnAppMenu() +{ + _native = [AvnMenu new]; +} + +AvnAppMenu::AvnAppMenu(AvnMenu* native) +{ + _native = native; +} + +AvnMenu* AvnAppMenu::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + + [_native addItem: avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::SetTitle (void* utf8String) +{ + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + + return S_OK; +} + +HRESULT AvnAppMenu::Clear() +{ + [_native removeAllItems]; + return S_OK; +} + +static IAvnAppMenu* s_AppMenu = nullptr; + +extern IAvnAppMenu* GetAppMenu() +{ + @autoreleasepool + { + if(s_AppMenu == nullptr) + { + id menubar = [NSMenu new]; + [menubar setTitle:@"Test"]; + [NSApp setMainMenu:menubar]; + + id appMenuItem = [AvnMenuItem new]; + [[NSApp mainMenu] addItem:appMenuItem]; + + [appMenuItem setSubmenu:[AvnMenu new]]; + + s_AppMenu = new AvnAppMenu([[NSApplication sharedApplication] mainMenu]); + } + + return s_AppMenu; + } +} + +extern IAvnAppMenu* CreateAppMenu() +{ + @autoreleasepool + { + return new AvnAppMenu(); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItem() +{ + @autoreleasepool + { + return new AvnAppMenuItem(); + } +} diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 297097584a..e7abedae51 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -10,12 +10,6 @@ class PlatformThreadingInterface; -(Signaler*) init; @end - -@interface ActionCallback : NSObject -- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; -- (void) action; -@end - @implementation ActionCallback { ComPtr _callback; From fde1e095ab6c2031d001d93e89a0724843445351 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 15:48:32 +0100 Subject: [PATCH 10/61] add previous osx menu implementation. --- .../AvaloniaNativeMenuExporter.cs | 151 ++++++++++++++++++ src/Avalonia.Native/WindowImpl.cs | 2 +- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 59a92a0ba8..647b867184 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -4,18 +4,56 @@ using System.Collections.Specialized; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform.Interop; using Avalonia.Threading; namespace Avalonia.Native { + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } + + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } + class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { + private IAvaloniaNativeFactory _factory; private NativeMenu _menu; private bool _resetQueued; private Dictionary _idsToItems = new Dictionary(); private Dictionary _itemsToIds = new Dictionary(); private uint _revision = 1; + public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) + { + _factory = factory; + } + public bool IsNativeMenuExported => throw new NotImplementedException(); public event EventHandler OnIsNativeMenuExportedChanged; @@ -73,6 +111,8 @@ namespace Avalonia.Native _revision++; LayoutUpdated?.Invoke((_revision, 0)); + + SetMenu(_menu.Items); } private void QueueReset() @@ -82,5 +122,116 @@ namespace Avalonia.Native _resetQueued = true; Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } + + private IAvnAppMenu CreateSubmenu(ICollection children) + { + var menu = _factory.CreateMenu(); + + SetChildren(menu, children); + + return menu; + } + + private void SetChildren(IAvnAppMenu menu, ICollection children) + { + foreach (var item in children) + { + var menuItem = _factory.CreateMenuItem(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null) + { + return item.Command.CanExecute(null); + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + menu.AddItem(menuItem); + + if (item.Menu?.Items?.Count > 0) + { + var submenu = _factory.CreateMenu(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + submenu.Title = buffer.DangerousGetHandle(); + } + + menuItem.SetSubMenu(submenu); + + AddItemsToMenu(submenu, item.Menu?.Items); + } + } + } + + private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) + { + foreach (var item in items) + { + var menuItem = _factory.CreateMenuItem(); + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null) + { + return item.Command.CanExecute(null); + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + + if (item.Menu?.Items.Count > 0 || isMainMenu) + { + var subMenu = CreateSubmenu(item.Menu?.Items); + + menuItem.SetSubMenu(subMenu); + + using (var buffer = new Utf8Buffer(item.Header)) + { + subMenu.Title = buffer.DangerousGetHandle(); + } + } + else + { + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + } + + menu.AddItem(menuItem); + } + } + + private void SetMenu(ICollection menuItems) + { + var appMenu = _factory.ObtainAppMenu(); + + appMenu.Clear(); + + AddItemsToMenu(appMenu, menuItems); + } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 76ee52f3ab..02c20b04ee 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native Init(_native = factory.CreateWindow(e), factory.CreateScreens()); } - NativeMenuExporter = new AvaloniaNativeMenuExporter(); + NativeMenuExporter = new AvaloniaNativeMenuExporter(factory); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents From 238855c5d1a50b133d019751216360913848b881 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 17:06:41 +0100 Subject: [PATCH 11/61] allow application name to be set on osx. --- samples/ControlCatalog/App.xaml.cs | 2 ++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 27 ++++++++++++++----- .../AvaloniaNativePlatformExtensions.cs | 12 +++++++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 07c42c60c4..3b758b0ba8 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -9,6 +9,8 @@ namespace ControlCatalog public override void Initialize() { AvaloniaXamlLoader.Load(this); + + Name = "Avalonia"; } public override void OnFrameworkInitializationCompleted() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6d48ab3829..3b26b6a60a 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,6 +9,7 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -27,15 +28,17 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO - public static void Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { - new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)) - .DoInitialize(options); + var result = new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)); + result.DoInitialize(options); + + return result; } delegate IntPtr CreateAvaloniaNativeDelegate(); - public static void Initialize(AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(AvaloniaNativePlatformOptions options) { if (options.AvaloniaNativeLibraryPath != null) { @@ -48,10 +51,21 @@ namespace Avalonia.Native var d = Marshal.GetDelegateForFunctionPointer(proc); - Initialize(d(), options); + return Initialize(d(), options); } else - Initialize(CreateAvaloniaNative(), options); + return Initialize(CreateAvaloniaNative(), options); + } + + public void SetupApplicationName() + { + if(!string.IsNullOrWhiteSpace(Application.Current.Name)) + { + using (var buffer = new Utf8Buffer(Application.Current.Name)) + { + _factory.MacOptions.SetApplicationTitle(buffer.DangerousGetHandle()); + } + } } private AvaloniaNativePlatform(IAvaloniaNativeFactory factory) @@ -66,6 +80,7 @@ namespace Avalonia.Native if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); + _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 02810ed155..df7b00ddd8 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -13,9 +13,17 @@ namespace Avalonia where T : AppBuilderBase, new() { builder.UseWindowingSubsystem(() => - AvaloniaNativePlatform.Initialize( + { + var platform = AvaloniaNativePlatform.Initialize( AvaloniaLocator.Current.GetService() ?? - new AvaloniaNativePlatformOptions())); + new AvaloniaNativePlatformOptions()); + + builder.AfterSetup (x=> + { + platform.SetupApplicationName(); + }); + }); + return builder; } } From aaedaebe94a113be8e452f59e08c1a17fb06cf28 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 17:15:17 +0100 Subject: [PATCH 12/61] working enabled when using click handlers. --- src/Avalonia.Controls/NativeMenu.cs | 1 - src/Avalonia.Controls/NativeMenuItem.cs | 2 ++ src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 13 ++++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 1e2966ff2b..e30029d21e 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -48,7 +48,6 @@ namespace Avalonia.Controls set => SetAndRaise(ParentProperty, ref _parent, value); } - public void Add(NativeMenuItem item) => _items.Add(item); public IEnumerator GetEnumerator() => _items.GetEnumerator(); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 3f1a80dcfe..e26176676a 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -110,6 +110,8 @@ namespace Avalonia.Controls Enabled = _command?.CanExecute(null) ?? true; } + public bool HasClickHandlers => Clicked != null; + public ICommand Command { get => GetValue(CommandProperty); diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 647b867184..1fade4b1a1 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -48,13 +48,14 @@ namespace Avalonia.Native private Dictionary _idsToItems = new Dictionary(); private Dictionary _itemsToIds = new Dictionary(); private uint _revision = 1; + private bool _exported = false; public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) { _factory = factory; } - public bool IsNativeMenuExported => throw new NotImplementedException(); + public bool IsNativeMenuExported => _exported; public event EventHandler OnIsNativeMenuExportedChanged; @@ -113,6 +114,8 @@ namespace Avalonia.Native LayoutUpdated?.Invoke((_revision, 0)); SetMenu(_menu.Items); + + _exported = true; } private void QueueReset() @@ -153,9 +156,9 @@ namespace Avalonia.Native menuItem.SetAction(new PredicateCallback(() => { - if (item.Command != null) + if (item.Command != null || item.HasClickHandlers) { - return item.Command.CanExecute(null); + return item.Enabled; } return false; @@ -186,9 +189,9 @@ namespace Avalonia.Native menuItem.SetAction(new PredicateCallback(() => { - if (item.Command != null) + if (item.Command != null || item.HasClickHandlers) { - return item.Command.CanExecute(null); + return item.Enabled; } return false; From e9d3b8ec5a484457c2eca8efdcc8869209699c45 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 17:17:50 +0100 Subject: [PATCH 13/61] support CMD in keygestures. --- src/Avalonia.Input/KeyGesture.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 5eaee4833c..7bf0fe6b70 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -141,6 +141,11 @@ namespace Avalonia.Input return KeyModifiers.Control; } + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Meta; + } + return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); } @@ -159,4 +164,4 @@ namespace Avalonia.Input } } } -} +} \ No newline at end of file From 5889c7feb812b320c052f0411474e9e4901f58fd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 17:19:04 +0100 Subject: [PATCH 14/61] use gesture for osx. --- samples/ControlCatalog/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index fca155ef70..882c7955d0 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -20,7 +20,7 @@ - + From ffcfeaa956d7fdd27e4a126c0f4334f1b4059a87 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 20:37:21 +0100 Subject: [PATCH 15/61] work towards menu per window. --- native/Avalonia.Native/inc/avalonia-native.h | 3 +- .../project.pbxproj | 2 -- native/Avalonia.Native/src/OSX/common.h | 1 - native/Avalonia.Native/src/OSX/main.mm | 6 ---- native/Avalonia.Native/src/OSX/menu.mm | 12 ++++---- native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 30 +++++++++++++++++++ .../AvaloniaNativeMenuExporter.cs | 6 ++-- 8 files changed, 44 insertions(+), 17 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 16d4fa4107..4cec243d0b 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -175,7 +175,6 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; - virtual HRESULT ObtainAppMenu (IAvnAppMenu** ppv) = 0; virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; }; @@ -208,6 +207,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; + virtual HRESULT GetMainMenu(IAvnAppMenu** ret) = 0; virtual bool TryLock() = 0; virtual void Unlock() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 84c3a84b91..a0c138b241 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; - 5296D43022F30EBC005B125D /* menu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = menu.h; path = ../../../../../../../../System/Volumes/Data/Users/danwalmsley/repos/Avalonia/native/Avalonia.Native/src/OSX/menu.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -89,7 +88,6 @@ 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, 520624B222973F4100C4DCEF /* menu.mm */, - 5296D43022F30EBC005B125D /* menu.h */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c066ebb498..ccd0a5a9b9 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,7 +19,6 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); -extern IAvnAppMenu* GetAppMenu(); extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 159f01d1d7..bdea26b761 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -225,12 +225,6 @@ public: return S_OK; } - virtual HRESULT ObtainAppMenu(IAvnAppMenu** ppv) override - { - *ppv = ::GetAppMenu(); - return S_OK; - } - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override { *ppv = ::CreateAppMenu(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index f861774b29..89ae31cc35 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -170,12 +170,13 @@ HRESULT AvnAppMenu::Clear() return S_OK; } -static IAvnAppMenu* s_AppMenu = nullptr; +//static IAvnAppMenu* s_AppMenu = nullptr; -extern IAvnAppMenu* GetAppMenu() +/*extern IAvnAppMenu* GetAppMenu() { @autoreleasepool { + //todo get rid of this method. if(s_AppMenu == nullptr) { id menubar = [NSMenu new]; @@ -183,7 +184,7 @@ extern IAvnAppMenu* GetAppMenu() [NSApp setMainMenu:menubar]; id appMenuItem = [AvnMenuItem new]; - [[NSApp mainMenu] addItem:appMenuItem]; + [menubar addItem:appMenuItem]; [appMenuItem setSubmenu:[AvnMenu new]]; @@ -192,13 +193,14 @@ extern IAvnAppMenu* GetAppMenu() return s_AppMenu; } -} +}*/ extern IAvnAppMenu* CreateAppMenu() { @autoreleasepool { - return new AvnAppMenu(); + id menuBar = [NSMenu new]; + return new AvnAppMenu(menuBar); } } diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e2221217f3..1e9cdcfbf8 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -20,6 +20,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; +-(void) setMenu:(NSMenu *)menu; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 3347d58004..205b1421d3 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -63,9 +63,11 @@ public: SoftwareDrawingOperation CurrentSwDrawingOperation; AvnPoint lastPositionSet; NSString* _lastTitle; + IAvnAppMenu* _mainMenu; WindowBaseImpl(IAvnWindowBaseEvents* events) { + _mainMenu = nullptr; BaseEvents = events; View = [[AvnView alloc] initWithParent:this]; @@ -209,6 +211,27 @@ public: } } + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + { + _mainMenu = menu; + + + + return S_OK; + } + + virtual HRESULT GetMainMenu(IAvnAppMenu** ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = _mainMenu; + + return S_OK; + } + virtual bool TryLock() override { @autoreleasepool @@ -1042,6 +1065,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; + NSMenu* _menu; } - (void)dealloc @@ -1065,6 +1089,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +-(void) setMenu:(NSMenu *)menu +{ + _menu = menu; + [NSApp setMenu:menu]; +} + -(void) setCanBecomeKeyAndMain { _canBecomeKeyAndMain = true; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1fade4b1a1..5e473d9aa8 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -114,7 +114,8 @@ namespace Avalonia.Native LayoutUpdated?.Invoke((_revision, 0)); SetMenu(_menu.Items); - + + _exported = true; } @@ -228,8 +229,9 @@ namespace Avalonia.Native } } - private void SetMenu(ICollection menuItems) + private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { + var appMenu = _factory.ObtainAppMenu(); appMenu.Clear(); From 448b549034f60bbe895dbf164ad79f7c0a012dd1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 21:01:51 +0100 Subject: [PATCH 16/61] set menu on per window basis. --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- .../AvaloniaNativeMenuExporter.cs | 17 ++++++++++++----- src/Avalonia.Native/WindowImpl.cs | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4cec243d0b..cbd90e1dcf 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -208,7 +208,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT GetMainMenu(IAvnAppMenu** ret) = 0; + virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; virtual bool TryLock() = 0; virtual void Unlock() = 0; }; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 5e473d9aa8..1ec5db149c 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -49,10 +49,12 @@ namespace Avalonia.Native private Dictionary _itemsToIds = new Dictionary(); private uint _revision = 1; private bool _exported = false; + private IAvnWindow _nativeWindow; - public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) + public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { _factory = factory; + _nativeWindow = nativeWindow; } public bool IsNativeMenuExported => _exported; @@ -113,8 +115,7 @@ namespace Avalonia.Native LayoutUpdated?.Invoke((_revision, 0)); - SetMenu(_menu.Items); - + SetMenu(_nativeWindow, _menu.Items); _exported = true; } @@ -231,8 +232,14 @@ namespace Avalonia.Native private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { - - var appMenu = _factory.ObtainAppMenu(); + var appMenu = avnWindow.ObtainMainMenu(); + + if(appMenu is null) + { + appMenu = _factory.CreateMenu(); + + avnWindow.SetMainMenu(appMenu); + } appMenu.Clear(); diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 02c20b04ee..a7828bedaf 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native Init(_native = factory.CreateWindow(e), factory.CreateScreens()); } - NativeMenuExporter = new AvaloniaNativeMenuExporter(factory); + NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents From 18def34bf74a2bade370a65e785f849e3d1d408c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 21:24:22 +0100 Subject: [PATCH 17/61] implement per window menus on osx side. --- .../OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj | 4 ++++ native/Avalonia.Native/src/OSX/menu.h | 2 +- native/Avalonia.Native/src/OSX/window.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 9 +++++++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index a0c138b241..c0a49382a7 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; @@ -25,6 +26,7 @@ /* Begin PBXFileReference section */ 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; + 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -87,6 +89,7 @@ AB661C1F2148286E00291242 /* window.mm */, 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, + 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, @@ -110,6 +113,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index e3c1fa9768..56cf0f6fe7 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -15,7 +15,7 @@ class AvnAppMenuItem; class AvnAppMenu; @interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain - +- (void)setMenu:(NSMenu*) menu; @end @interface AvnMenuItem : NSMenuItem diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 1e9cdcfbf8..557e19e7a8 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -20,7 +20,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) setMenu:(NSMenu *)menu; +-(void) applyMenu:(NSMenu *)menu; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 205b1421d3..39c9ceec13 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,6 +5,7 @@ #include "window.h" #include "KeyTransform.h" #include "cursor.h" +#include "menu.h" #include class SoftwareDrawingOperation @@ -215,12 +216,16 @@ public: { _mainMenu = menu; + auto nativeMenu = dynamic_cast(menu); + auto nsmenu = nativeMenu->GetNative(); + + [Window applyMenu:nsmenu]; return S_OK; } - virtual HRESULT GetMainMenu(IAvnAppMenu** ret) override + virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override { if(ret == nullptr) { @@ -1089,7 +1094,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(void) setMenu:(NSMenu *)menu +-(void) applyMenu:(NSMenu *)menu { _menu = menu; [NSApp setMenu:menu]; From f5020af86032daaad025ea4696c3d5463cc83f17 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 21:38:15 +0100 Subject: [PATCH 18/61] change window menus when windows are changed. --- native/Avalonia.Native/src/OSX/window.mm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 39c9ceec13..21e190695e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1192,6 +1192,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { + if(_menu == nullptr) + { + [NSApp setMenu: [NSMenu new]]; + } + else + { + [NSApp setMenu:_menu]; + } + _parent->BaseEvents->Activated(); [super becomeKeyWindow]; } From ec6f1e824f46a8bad4e364441285d70b56ce8380 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 21:41:21 +0100 Subject: [PATCH 19/61] tidy avnwindow menu code. --- native/Avalonia.Native/src/OSX/menu.mm | 25 ------------------------ native/Avalonia.Native/src/OSX/window.mm | 13 +++++++----- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 89ae31cc35..909a504dc7 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -170,31 +170,6 @@ HRESULT AvnAppMenu::Clear() return S_OK; } -//static IAvnAppMenu* s_AppMenu = nullptr; - -/*extern IAvnAppMenu* GetAppMenu() -{ - @autoreleasepool - { - //todo get rid of this method. - if(s_AppMenu == nullptr) - { - id menubar = [NSMenu new]; - [menubar setTitle:@"Test"]; - [NSApp setMainMenu:menubar]; - - id appMenuItem = [AvnMenuItem new]; - [menubar addItem:appMenuItem]; - - [appMenuItem setSubmenu:[AvnMenu new]]; - - s_AppMenu = new AvnAppMenu([[NSApplication sharedApplication] mainMenu]); - } - - return s_AppMenu; - } -}*/ - extern IAvnAppMenu* CreateAppMenu() { @autoreleasepool diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 21e190695e..baa8be8e05 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1096,6 +1096,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(void) applyMenu:(NSMenu *)menu { + if(menu == nullptr) + { + menu = [NSMenu new]; + } + _menu = menu; [NSApp setMenu:menu]; } @@ -1194,13 +1199,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(_menu == nullptr) { - [NSApp setMenu: [NSMenu new]]; - } - else - { - [NSApp setMenu:_menu]; + _menu = [NSMenu new]; } + [NSApp setMenu:_menu]; + _parent->BaseEvents->Activated(); [super becomeKeyWindow]; } From 034e0a1c2bfcec2c5411477756e095edddca0e4b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 22:02:50 +0100 Subject: [PATCH 20/61] fix osx menu cant be selected when running under xcode. --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index baa8be8e05..e7a2b976e5 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -96,6 +96,7 @@ public: UpdateStyle(); [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; [Window setTitleVisibility:NSWindowTitleVisible]; @@ -125,6 +126,7 @@ public: if(Window != nullptr) { [Window makeKeyWindow]; + [NSApp activateIgnoringOtherApps:YES]; } } From 72a9a21299a9bb714d443a862637dfb694e20cfd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 22:15:16 +0100 Subject: [PATCH 21/61] osx native menu items respond to model changes. --- .../AvaloniaNativeMenuExporter.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1ec5db149c..6c7e5f63e3 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -44,12 +44,11 @@ namespace Avalonia.Native { private IAvaloniaNativeFactory _factory; private NativeMenu _menu; - private bool _resetQueued; - private Dictionary _idsToItems = new Dictionary(); - private Dictionary _itemsToIds = new Dictionary(); + private bool _resetQueued; private uint _revision = 1; private bool _exported = false; private IAvnWindow _nativeWindow; + private List _menuItems = new List(); public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -101,19 +100,18 @@ namespace Avalonia.Native void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) + foreach (var i in _menuItems) { i.PropertyChanged -= OnItemPropertyChanged; if (i.Menu != null) ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; } - _idsToItems.Clear(); - _itemsToIds.Clear(); + _menuItems.Clear(); _revision++; - LayoutUpdated?.Invoke((_revision, 0)); + LayoutUpdated?.Invoke((_revision, 0)); SetMenu(_nativeWindow, _menu.Items); @@ -137,10 +135,20 @@ namespace Avalonia.Native return menu; } + private void AddMenuItem(NativeMenuItem item) + { + if(item.Menu?.Items != null) + { + ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + } + } + private void SetChildren(IAvnAppMenu menu, ICollection children) { foreach (var item in children) { + AddMenuItem(item); + var menuItem = _factory.CreateMenuItem(); using (var buffer = new Utf8Buffer(item.Header)) @@ -189,6 +197,8 @@ namespace Avalonia.Native { var menuItem = _factory.CreateMenuItem(); + AddMenuItem(item); + menuItem.SetAction(new PredicateCallback(() => { if (item.Command != null || item.HasClickHandlers) From 1d6d18503812ba592330c3f22ae416654a5d7faf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Sep 2019 22:21:53 +0100 Subject: [PATCH 22/61] remove unused code. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 6c7e5f63e3..16f2cc32b7 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -44,8 +44,7 @@ namespace Avalonia.Native { private IAvaloniaNativeFactory _factory; private NativeMenu _menu; - private bool _resetQueued; - private uint _revision = 1; + private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; private List _menuItems = new List(); @@ -60,8 +59,6 @@ namespace Avalonia.Native public event EventHandler OnIsNativeMenuExportedChanged; - private event Action<(uint revision, int parent)> LayoutUpdated; - public void SetNativeMenu(NativeMenu menu) { if (menu == null) @@ -107,11 +104,7 @@ namespace Avalonia.Native ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; } - _menuItems.Clear(); - - _revision++; - - LayoutUpdated?.Invoke((_revision, 0)); + _menuItems.Clear(); SetMenu(_nativeWindow, _menu.Items); From 92e0e2bccc14d3d855f897970876050e76c36a98 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 20:30:11 +0100 Subject: [PATCH 23/61] attempt at application menu pre-pending. --- samples/ControlCatalog/App.xaml | 32 +++++++++++++++++-- samples/ControlCatalog/MainWindow.xaml | 2 +- .../AvaloniaNativeMenuExporter.cs | 15 ++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 2f6d25c089..8facde71c1 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,9 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 882c7955d0..1b01fa56a8 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow"> + x:Class="ControlCatalog.MainWindow" NativeMenu.PrependApplicationMenu="True"> diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 16f2cc32b7..bc626add91 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Platform; @@ -47,6 +48,7 @@ namespace Avalonia.Native private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; + private bool _prependAppMenu; private List _menuItems = new List(); public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) @@ -74,7 +76,7 @@ namespace Avalonia.Native public void SetPrependApplicationMenu(bool prepend) { - throw new NotImplementedException(); + _prependAppMenu = prepend; } private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) @@ -235,6 +237,17 @@ namespace Avalonia.Native private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { + if (_prependAppMenu) + { + var menu = NativeMenu.GetMenu(Application.Current); + + var items = menuItems.ToList(); + + items.InsertRange(0, menu.Items); + + menuItems = items; + } + var appMenu = avnWindow.ObtainMainMenu(); if(appMenu is null) From 13f78139843dae704e0ab2e4a49cc26a2527cdc4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 20:32:22 +0100 Subject: [PATCH 24/61] add support for , in key gesture. --- src/Avalonia.Input/KeyGesture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 7bf0fe6b70..490c31bef9 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input { private static readonly Dictionary s_keySynonyms = new Dictionary { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod } + { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } }; [Obsolete("Use constructor taking KeyModifiers")] @@ -164,4 +164,4 @@ namespace Avalonia.Input } } } -} \ No newline at end of file +} From aa37fa40d52315d658136e236aa0b9123accfa59 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 20:36:47 +0100 Subject: [PATCH 25/61] osx always exports app menu if it exists. --- .../AvaloniaNativeMenuExporter.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index bc626add91..dc713df56c 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -45,11 +45,10 @@ namespace Avalonia.Native { private IAvaloniaNativeFactory _factory; private NativeMenu _menu; - private bool _resetQueued; + private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; - private bool _prependAppMenu; - private List _menuItems = new List(); + private List _menuItems = new List(); public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -76,7 +75,7 @@ namespace Avalonia.Native public void SetPrependApplicationMenu(bool prepend) { - _prependAppMenu = prepend; + // OSX always exports the app menu. } private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) @@ -106,10 +105,10 @@ namespace Avalonia.Native ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; } - _menuItems.Clear(); + _menuItems.Clear(); SetMenu(_nativeWindow, _menu.Items); - + _exported = true; } @@ -132,7 +131,7 @@ namespace Avalonia.Native private void AddMenuItem(NativeMenuItem item) { - if(item.Menu?.Items != null) + if (item.Menu?.Items != null) { ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; } @@ -237,10 +236,10 @@ namespace Avalonia.Native private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { - if (_prependAppMenu) - { - var menu = NativeMenu.GetMenu(Application.Current); + var menu = NativeMenu.GetMenu(Application.Current); + if (menu != null) + { var items = menuItems.ToList(); items.InsertRange(0, menu.Items); @@ -250,7 +249,7 @@ namespace Avalonia.Native var appMenu = avnWindow.ObtainMainMenu(); - if(appMenu is null) + if (appMenu is null) { appMenu = _factory.CreateMenu(); From c17058c9aa86b4e00db59973eee423dff4e71ac2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 20:54:33 +0100 Subject: [PATCH 26/61] implement app menu osx. --- native/Avalonia.Native/src/OSX/menu.mm | 5 ++++- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 909a504dc7..ca03c50e59 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -159,7 +159,10 @@ HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) HRESULT AvnAppMenu::SetTitle (void* utf8String) { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } return S_OK; } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index dc713df56c..1ae4a783d7 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -54,6 +54,8 @@ namespace Avalonia.Native { _factory = factory; _nativeWindow = nativeWindow; + + DoLayoutReset(); } public bool IsNativeMenuExported => _exported; @@ -107,7 +109,7 @@ namespace Avalonia.Native _menuItems.Clear(); - SetMenu(_nativeWindow, _menu.Items); + SetMenu(_nativeWindow, _menu?.Items); _exported = true; } @@ -236,6 +238,11 @@ namespace Avalonia.Native private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { + if (menuItems is null) + { + menuItems = new List(); + } + var menu = NativeMenu.GetMenu(Application.Current); if (menu != null) From 0a1269636f82be376220952e25c8ac92c0e069db Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 21:32:44 +0100 Subject: [PATCH 27/61] add a menu to decorated window. --- samples/ControlCatalog/DecoratedWindow.xaml | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index cb6016b324..8e4c97b7f0 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -3,6 +3,31 @@ x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window"> + + + + + + + + + + + + + + + + + + + + + + + + + From 50fa18b6e1b58990d7bd04430da67a9b769e82d6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 21:45:46 +0100 Subject: [PATCH 28/61] fix wierd menu in menu issue on dialogs. --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index e7a2b976e5..78ba930c9c 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1250,6 +1250,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(_parent) _parent->BaseEvents->Deactivated(); + + [NSApp setMenu:nullptr]; [super resignKeyWindow]; } From 4bd7c3960980fc978c078d0dbe9102cd0f16b941 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 23:46:04 +0100 Subject: [PATCH 29/61] working reparenting of app menu. --- native/Avalonia.Native/inc/avalonia-native.h | 2 + native/Avalonia.Native/src/OSX/common.h | 3 ++ native/Avalonia.Native/src/OSX/main.mm | 18 +++++++ native/Avalonia.Native/src/OSX/menu.mm | 31 +++++++++++ native/Avalonia.Native/src/OSX/window.mm | 51 ++++++++++++++++++- .../AvaloniaNativeMenuExporter.cs | 44 ++++++++++++---- src/Avalonia.Native/AvaloniaNativePlatform.cs | 7 ++- .../AvaloniaNativePlatformExtensions.cs | 1 + 8 files changed, 145 insertions(+), 12 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index cbd90e1dcf..7d9b89852e 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -175,6 +175,8 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; }; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index ccd0a5a9b9..91a1ba51c3 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -21,6 +21,9 @@ extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); +extern void SetAppMenu (IAvnAppMenu* appMenu); +extern IAvnAppMenu* GetAppMenu (); +extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index bdea26b761..ade077f00b 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -236,6 +236,24 @@ public: *ppv = ::CreateAppMenuItem(); return S_OK; } + + virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + { + ::SetAppMenu(appMenu); + return S_OK; + } + + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = ::GetAppMenu(); + + return S_OK; + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index ca03c50e59..9aec33d3db 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -189,3 +189,34 @@ extern IAvnAppMenuItem* CreateAppMenuItem() return new AvnAppMenuItem(); } } + +static IAvnAppMenu* s_appMenu = nullptr; +static NSMenuItem* s_appMenuItem = nullptr; + +extern void SetAppMenu (IAvnAppMenu* appMenu) +{ + s_appMenu = appMenu; + + if(s_appMenu != nullptr) + { + auto nativeMenu = dynamic_cast(s_appMenu); + + s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + } + else + { + s_appMenuItem = nullptr; + } +} + +extern IAvnAppMenu* GetAppMenu () +{ + return s_appMenu; +} + +extern NSMenuItem* GetAppMenuItem () +{ + return s_appMenuItem; +} + + diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 78ba930c9c..dbb437243a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1073,6 +1073,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _canBecomeKeyAndMain; bool _closed; NSMenu* _menu; + bool _isAppMenuApplied; } - (void)dealloc @@ -1104,7 +1105,22 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } _menu = menu; - [NSApp setMenu:menu]; + + if ([self isKeyWindow]) + { + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + + [NSApp setMenu:menu]; + } } -(void) setCanBecomeKeyAndMain @@ -1204,6 +1220,17 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _menu = [NSMenu new]; } + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + [NSApp setMenu:_menu]; _parent->BaseEvents->Activated(); @@ -1251,7 +1278,27 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_parent) _parent->BaseEvents->Deactivated(); - [NSApp setMenu:nullptr]; + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; + + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; + } + + // remove window menu items from appmenu? + [super resignKeyWindow]; } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1ae4a783d7..40159077d5 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -58,6 +58,14 @@ namespace Avalonia.Native DoLayoutReset(); } + public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) + { + _factory = factory; + + _menu = NativeMenu.GetMenu(Application.Current); + DoLayoutReset(); + } + public bool IsNativeMenuExported => _exported; public event EventHandler OnIsNativeMenuExportedChanged; @@ -109,7 +117,14 @@ namespace Avalonia.Native _menuItems.Clear(); - SetMenu(_nativeWindow, _menu?.Items); + if(_nativeWindow is null) + { + SetMenu(_menu?.Items); + } + else + { + SetMenu(_nativeWindow, _menu?.Items); + } _exported = true; } @@ -236,7 +251,7 @@ namespace Avalonia.Native } } - private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + private void SetMenu(ICollection menuItems) { if (menuItems is null) { @@ -247,11 +262,24 @@ namespace Avalonia.Native if (menu != null) { - var items = menuItems.ToList(); + var appMenu = _factory.ObtainAppMenu (); - items.InsertRange(0, menu.Items); + if (appMenu is null) + { + appMenu = _factory.CreateMenu(); + } - menuItems = items; + AddItemsToMenu(appMenu, menuItems); + + _factory.SetAppMenu(appMenu); + } + } + + private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + { + if (menuItems is null) + { + menuItems = new List(); } var appMenu = avnWindow.ObtainMainMenu(); @@ -259,13 +287,11 @@ namespace Avalonia.Native if (appMenu is null) { appMenu = _factory.CreateMenu(); - - avnWindow.SetMainMenu(appMenu); } - appMenu.Clear(); - AddItemsToMenu(appMenu, menuItems); + + avnWindow.SetMainMenu(appMenu); } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 3b26b6a60a..ddb71b61bb 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -57,7 +57,12 @@ namespace Avalonia.Native return Initialize(CreateAvaloniaNative(), options); } - public void SetupApplicationName() + public void SetupApplicationMenuExporter () + { + var exporter = new AvaloniaNativeMenuExporter(_factory); + } + + public void SetupApplicationName () { if(!string.IsNullOrWhiteSpace(Application.Current.Name)) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index df7b00ddd8..a22777d5eb 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -20,6 +20,7 @@ namespace Avalonia builder.AfterSetup (x=> { + platform.SetupApplicationMenuExporter(); platform.SetupApplicationName(); }); }); From 796221d7f35221a843f2430fcb3b47b16d14505f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Sep 2019 23:56:58 +0100 Subject: [PATCH 30/61] remove IGetNative interface --- native/Avalonia.Native/inc/IGetNative.h | 10 ---------- native/Avalonia.Native/src/OSX/menu.mm | 1 - 2 files changed, 11 deletions(-) delete mode 100644 native/Avalonia.Native/inc/IGetNative.h diff --git a/native/Avalonia.Native/inc/IGetNative.h b/native/Avalonia.Native/inc/IGetNative.h deleted file mode 100644 index 85ae030d74..0000000000 --- a/native/Avalonia.Native/inc/IGetNative.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef igetnative_h -#define igetnative_h - -class IGetNative -{ -public: - virtual void* GetNative() = 0; -}; - -#endif diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 9aec33d3db..cbcef277cc 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,6 +1,5 @@ #include "common.h" -#include "IGetNative.h" #include "menu.h" @implementation AvnMenu From 5736a7965de9f4323a5d3012d45bbf63be330cca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 10:34:18 +0100 Subject: [PATCH 31/61] add support for seperators --- samples/ControlCatalog/App.xaml | 4 + src/Avalonia.Controls/NativeMenu.cs | 14 +- src/Avalonia.Controls/NativeMenuItem.cs | 55 +------ src/Avalonia.Controls/NativeMenuItemBase.cs | 56 +++++++ .../NativeMenuItemSeperator.cs | 7 + .../AvaloniaNativeMenuExporter.cs | 141 +++++++++--------- 6 files changed, 149 insertions(+), 128 deletions(-) create mode 100644 src/Avalonia.Controls/NativeMenuItemBase.cs create mode 100644 src/Avalonia.Controls/NativeMenuItemSeperator.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 8facde71c1..a82996773f 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -24,7 +24,9 @@ + + @@ -34,9 +36,11 @@ + + diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index e30029d21e..8d3dcd9f8f 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -9,13 +9,13 @@ using Avalonia.Metadata; namespace Avalonia.Controls { - public partial class NativeMenu : AvaloniaObject, IEnumerable + public partial class NativeMenu : AvaloniaObject, IEnumerable { - private AvaloniaList _items = - new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; + private AvaloniaList _items = + new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; [Content] - public IList Items => _items; + public IList Items => _items; public NativeMenu() { @@ -23,7 +23,7 @@ namespace Avalonia.Controls _items.CollectionChanged += ItemsChanged; } - private void Validator(NativeMenuItem obj) + private void Validator(NativeMenuItemBase obj) { if (obj.Parent != null) throw new InvalidOperationException("NativeMenuItem already has a parent"); @@ -48,9 +48,9 @@ namespace Avalonia.Controls set => SetAndRaise(ParentProperty, ref _parent, value); } - public void Add(NativeMenuItem item) => _items.Add(item); + public void Add(NativeMenuItemBase item) => _items.Add(item); - public IEnumerator GetEnumerator() => _items.GetEnumerator(); + public IEnumerator GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index e26176676a..25e0c9e48e 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -1,20 +1,16 @@ 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 + public class NativeMenuItem : NativeMenuItemBase { private string _header; private KeyGesture _gesture; private bool _enabled = true; - private NativeMenu _menu; - private NativeMenu _parent; + class CanExecuteChangedSubscriber : IWeakSubscriber { @@ -33,18 +29,7 @@ namespace Avalonia.Controls 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); @@ -54,7 +39,7 @@ namespace Avalonia.Controls { Header = header; } - + public static readonly DirectProperty HeaderProperty = AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); @@ -65,7 +50,7 @@ namespace Avalonia.Controls } public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o,v)=> o._gesture = v); + AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); public KeyGesture Gesture { @@ -128,36 +113,6 @@ namespace Avalonia.Controls set { SetValue(CommandParameterProperty, value); } } - public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(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 ParentProperty = - AvaloniaProperty.RegisterDirect("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() diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs new file mode 100644 index 0000000000..e17ed86054 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -0,0 +1,56 @@ +using System; + +namespace Avalonia.Controls +{ + public class NativeMenuItemBase : AvaloniaObject + { + private NativeMenu _menu; + private NativeMenu _parent; + + static NativeMenuItemBase() + { + 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; + }); + } + + internal NativeMenuItemBase() + { + + } + + public static readonly DirectProperty MenuProperty = + AvaloniaProperty.RegisterDirect(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 ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + + public NativeMenu Parent + { + get => _parent; + set => SetAndRaise(ParentProperty, ref _parent, value); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs new file mode 100644 index 0000000000..85d62023d4 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemSeperator.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls +{ + public class NativeMenuItemSeperator : NativeMenuItemBase + { + + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 40159077d5..31ccfab18c 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -98,13 +98,6 @@ namespace Avalonia.Native QueueReset(); } - /* - 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; @@ -137,7 +130,7 @@ namespace Avalonia.Native Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private IAvnAppMenu CreateSubmenu(ICollection children) + private IAvnAppMenu CreateSubmenu(ICollection children) { var menu = _factory.CreateMenu(); @@ -154,108 +147,114 @@ namespace Avalonia.Native } } - private void SetChildren(IAvnAppMenu menu, ICollection children) + private void SetChildren(IAvnAppMenu menu, ICollection children) { - foreach (var item in children) + foreach (var i in children) { - AddMenuItem(item); - - var menuItem = _factory.CreateMenuItem(); - - using (var buffer = new Utf8Buffer(item.Header)) + if (i is NativeMenuItem item) { - menuItem.Title = buffer.DangerousGetHandle(); - } + AddMenuItem(item); - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + var menuItem = _factory.CreateMenuItem(); + + using (var buffer = new Utf8Buffer(item.Header)) { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + menuItem.Title = buffer.DangerousGetHandle(); } - } - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) + if (item.Gesture != null) { - return item.Enabled; + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } } - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - menu.AddItem(menuItem); + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } - if (item.Menu?.Items?.Count > 0) - { - var submenu = _factory.CreateMenu(); + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + menu.AddItem(menuItem); - using (var buffer = new Utf8Buffer(item.Header)) + if (item.Menu?.Items?.Count > 0) { - submenu.Title = buffer.DangerousGetHandle(); - } + var submenu = _factory.CreateMenu(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + submenu.Title = buffer.DangerousGetHandle(); + } - menuItem.SetSubMenu(submenu); + menuItem.SetSubMenu(submenu); - AddItemsToMenu(submenu, item.Menu?.Items); + AddItemsToMenu(submenu, item.Menu?.Items); + } } } } - private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) + private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) { - foreach (var item in items) + foreach (var i in items) { - var menuItem = _factory.CreateMenuItem(); + if (i is NativeMenuItem item) + { + var menuItem = _factory.CreateMenuItem(); - AddMenuItem(item); + AddMenuItem(item); - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) + menuItem.SetAction(new PredicateCallback(() => { - return item.Enabled; - } + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); - if (item.Menu?.Items.Count > 0 || isMainMenu) - { - var subMenu = CreateSubmenu(item.Menu?.Items); + if (item.Menu?.Items.Count > 0 || isMainMenu) + { + var subMenu = CreateSubmenu(item.Menu?.Items); - menuItem.SetSubMenu(subMenu); + menuItem.SetSubMenu(subMenu); - using (var buffer = new Utf8Buffer(item.Header)) - { - subMenu.Title = buffer.DangerousGetHandle(); + using (var buffer = new Utf8Buffer(item.Header)) + { + subMenu.Title = buffer.DangerousGetHandle(); + } } - } - else - { - using (var buffer = new Utf8Buffer(item.Header)) + else { - menuItem.Title = buffer.DangerousGetHandle(); - } + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + if (item.Gesture != null) { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } } } - } - menu.AddItem(menuItem); + menu.AddItem(menuItem); + } } } - private void SetMenu(ICollection menuItems) + private void SetMenu(ICollection menuItems) { if (menuItems is null) { - menuItems = new List(); + menuItems = new List(); } var menu = NativeMenu.GetMenu(Application.Current); @@ -275,11 +274,11 @@ namespace Avalonia.Native } } - private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) { if (menuItems is null) { - menuItems = new List(); + menuItems = new List(); } var appMenu = avnWindow.ObtainMainMenu(); From 1a3506cbcbbcbedac4fb2729ba1e0ed47e8f7ee8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 10:49:41 +0100 Subject: [PATCH 32/61] implement seperator support on native side. --- native/Avalonia.Native/inc/avalonia-native.h | 1 + native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 6 +++++ native/Avalonia.Native/src/OSX/menu.h | 7 ++--- native/Avalonia.Native/src/OSX/menu.mm | 27 ++++++++++++++++---- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 7d9b89852e..f1c7664c3e 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -179,6 +179,7 @@ public: virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 91a1ba51c3..c91f562989 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -21,6 +21,7 @@ extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); +extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); extern void SetAppMenu (IAvnAppMenu* appMenu); extern IAvnAppMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index ade077f00b..1a46e495ba 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -237,6 +237,12 @@ public: return S_OK; } + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItemSeperator(); + return S_OK; + } + virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override { ::SetAppMenu(appMenu); diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 56cf0f6fe7..befbe6a7e0 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -26,16 +26,17 @@ class AvnAppMenu; class AvnAppMenuItem : public ComSingleObject { private: - AvnMenuItem* _native; // here we hold a pointer to an AvnMenuItem + NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; + bool _isSeperator; public: FORWARD_IUNKNOWN() - AvnAppMenuItem(); + AvnAppMenuItem(bool isSeperator); - AvnMenuItem* GetNative(); + NSMenuItem* GetNative(); virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index cbcef277cc..397d587ec3 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -43,13 +43,23 @@ } @end -AvnAppMenuItem::AvnAppMenuItem() +AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { - _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + _isSeperator = isSeperator; + + if(isSeperator) + { + _native = [NSMenuItem separatorItem]; + } + else + { + _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + } + _callback = nullptr; } -AvnMenuItem* AvnAppMenuItem::GetNative() +NSMenuItem* AvnAppMenuItem::GetNative() { return _native; } @@ -137,7 +147,6 @@ HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) if(avnMenuItem != nullptr) { - [_native addItem: avnMenuItem->GetNative()]; } @@ -185,7 +194,15 @@ extern IAvnAppMenuItem* CreateAppMenuItem() { @autoreleasepool { - return new AvnAppMenuItem(); + return new AvnAppMenuItem(false); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +{ + @autoreleasepool + { + return new AvnAppMenuItem(true); } } From 0dabaee113053f420f4743acde52f5277f4616f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 10:56:47 +0100 Subject: [PATCH 33/61] make dbus menu exporter compile. --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 99 +++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 1f1a244fb7..1a9a7bc231 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -31,8 +31,8 @@ namespace Avalonia.FreeDesktop private bool _disposed; private uint _revision = 1; private NativeMenu _menu; - private Dictionary _idsToItems = new Dictionary(); - private Dictionary _itemsToIds = new Dictionary(); + private Dictionary _idsToItems = new Dictionary(); + private Dictionary _itemsToIds = new Dictionary(); private bool _resetQueued; private int _nextId = 1; public DBusMenuExporterImpl(Connection dbus, IntPtr xid) @@ -124,7 +124,7 @@ namespace Avalonia.FreeDesktop Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private (NativeMenuItem item, NativeMenu menu) GetMenu(int id) + private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) { if (id == 0) return (null, _menu); @@ -132,7 +132,7 @@ namespace Avalonia.FreeDesktop return (item, item?.Menu); } - private int GetId(NativeMenuItem item) + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) return id; @@ -186,54 +186,59 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" }; - object GetProperty((NativeMenuItem item, NativeMenu menu) i, string name) + object GetProperty((NativeMenuItemBase 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 ?? ""; - 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") + var (it, menu) = i; + + if (it is NativeMenuItem item) { - if (item?.Gesture == null) + if (name == "type") + { + if (item != null && item.Header == null) + return "separator"; return null; - if (item.Gesture.KeyModifiers == 0) + } + if (name == "label") + return item?.Header ?? ""; + 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; - var lst = new List(); - 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 == "shortcut") + { + if (item?.Gesture == null) + return null; + if (item.Gesture.KeyModifiers == 0) + return null; + var lst = new List(); + 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; } - if (name == "children-display") - return menu != null ? "submenu" : null; return null; } private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItem item, NativeMenu menu) i, string[] names) + KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) { if (names?.Length > 0 != true) names = AllProperties; @@ -267,7 +272,7 @@ namespace Avalonia.FreeDesktop return Task.FromResult(rv); } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItem item, NativeMenu menu, int depth, string[] propertyNames) + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) { var id = item == null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); @@ -308,8 +313,12 @@ namespace Avalonia.FreeDesktop if (eventId == "clicked") { var item = GetMenu(id).item; - if (item?.Enabled == true) - item.RaiseClick(); + + if (item is NativeMenuItem menuItem) + { + if (menuItem?.Enabled == true) + menuItem.RaiseClick(); + } } } From 2baa06f344176b7a38b7a50eb4956854624ad208 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 11:00:34 +0100 Subject: [PATCH 34/61] osx add native seperators to menu --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 31ccfab18c..93e0dba73b 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -195,6 +195,10 @@ namespace Avalonia.Native AddItemsToMenu(submenu, item.Menu?.Items); } } + else if (i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } } } @@ -247,6 +251,10 @@ namespace Avalonia.Native menu.AddItem(menuItem); } + else if(i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } } } From 2ad9dc56e8b0fd68ebd1f60c00ddf696c572f4b4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 11:05:43 +0100 Subject: [PATCH 35/61] fix native menu cast error. --- src/Avalonia.Controls/NativeMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 8d3dcd9f8f..58cff581e1 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -32,10 +32,10 @@ namespace Avalonia.Controls private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { if(e.OldItems!=null) - foreach (NativeMenuItem i in e.OldItems) + foreach (NativeMenuItemBase i in e.OldItems) i.Parent = null; if(e.NewItems!=null) - foreach (NativeMenuItem i in e.NewItems) + foreach (NativeMenuItemBase i in e.NewItems) i.Parent = this; } From d2848809a2d100956761851e456796665601960b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:14:18 +0100 Subject: [PATCH 36/61] Implement auto generate standard osx app menu items. --- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 14 +++++-- native/Avalonia.Native/src/OSX/menu.mm | 49 ++++++++++++++++++++++++- samples/ControlCatalog/App.xaml | 15 -------- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c91f562989..10534dea26 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,7 +22,7 @@ extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); -extern void SetAppMenu (IAvnAppMenu* appMenu); +extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); extern IAvnAppMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 1a46e495ba..9418782fd1 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,10 +5,16 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +static NSString* s_appTitle = @"Avalonia"; + // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -void SetProcessName(CFStringRef process_name) { +void SetProcessName(NSString* appTitle) { + s_appTitle = appTitle; + + CFStringRef process_name = (__bridge CFStringRef)appTitle; + if (!process_name || CFStringGetLength(process_name) == 0) { //NOTREACHED() << "SetProcessName given bad name."; return; @@ -108,8 +114,8 @@ public: [[NSProcessInfo processInfo] setProcessName:appTitle]; - CFStringRef titleRef = (__bridge CFStringRef)appTitle; - SetProcessName(titleRef); + + SetProcessName(appTitle); return S_OK; } @@ -245,7 +251,7 @@ public: virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override { - ::SetAppMenu(appMenu); + ::SetAppMenu(s_appTitle, appMenu); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 397d587ec3..84dc80806a 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -209,15 +209,60 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator() static IAvnAppMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (IAvnAppMenu* appMenu) +extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) { - s_appMenu = appMenu; + s_appMenu = menu; if(s_appMenu != nullptr) { auto nativeMenu = dynamic_cast(s_appMenu); s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + + auto appMenu = [s_appMenuItem submenu]; + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Services item and menu + auto servicesItem = [[NSMenuItem alloc] init]; + servicesItem.title = @"Services"; + NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + servicesItem.submenu = servicesMenu; + [NSApplication sharedApplication].servicesMenu = servicesMenu; + [appMenu addItem:servicesItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Hide Application + auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; + //hideItem.target = self; + [appMenu addItem:hideItem]; + + // Hide Others + auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"]; + //hideAllOthersItem.target = self; + hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; + [appMenu addItem:hideAllOthersItem]; + + // Show All + auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + //showAllItem.target = self; + [appMenu addItem:showAllItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Quit Application + auto quitItem = [[NSMenuItem alloc] init]; + quitItem.title = [@"Quit " stringByAppendingString:appName]; + quitItem.keyEquivalent = @"q"; + // This will remain true until synced with a QCocoaMenuItem. + // This way, we will always have a functional Quit menu item + // even if no QAction is added. + quitItem.action = @selector(terminate:); + [appMenu addItem:quitItem]; } else { diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index a82996773f..726a3c915e 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -27,21 +27,6 @@ - - - - - - - - - - - - - - - From 08fa46288134c7822af9f96b376cd1305d8995f5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:15:08 +0100 Subject: [PATCH 37/61] [OSX platform] set app title before setting exporter. --- src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index a22777d5eb..091056142f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -20,8 +20,8 @@ namespace Avalonia builder.AfterSetup (x=> { - platform.SetupApplicationMenuExporter(); platform.SetupApplicationName(); + platform.SetupApplicationMenuExporter(); }); }); From 8463fb00933e37d787c15932f0196e611d780a10 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:15:49 +0100 Subject: [PATCH 38/61] submenu only on menuitems not seprators --- src/Avalonia.Controls/NativeMenuItem.cs | 34 +++++++++++++++++++++ src/Avalonia.Controls/NativeMenuItemBase.cs | 33 -------------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 25e0c9e48e..c1144d45b2 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -11,6 +11,20 @@ namespace Avalonia.Controls private KeyGesture _gesture; private bool _enabled = true; + private NativeMenu _menu; + + 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; + }); + } + class CanExecuteChangedSubscriber : IWeakSubscriber { @@ -40,6 +54,26 @@ namespace Avalonia.Controls Header = header; } + public static readonly DirectProperty MenuProperty = + AvaloniaProperty.RegisterDirect(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 HeaderProperty = AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index e17ed86054..47eb86cdc3 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -4,46 +4,13 @@ namespace Avalonia.Controls { public class NativeMenuItemBase : AvaloniaObject { - private NativeMenu _menu; private NativeMenu _parent; - static NativeMenuItemBase() - { - 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; - }); - } - internal NativeMenuItemBase() { } - public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(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 ParentProperty = AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); From 1e00b3fe3f22e5f7a42fab157fbfe67ac6e393af Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:25:39 +0100 Subject: [PATCH 39/61] fix build --- samples/ControlCatalog/MainWindow.xaml.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 9f62c0da38..819ab0655a 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -30,7 +30,7 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); - _recentMenu = NativeMenu.GetMenu(this).Items[0].Menu.Items[1].Menu; + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 1a9a7bc231..0ab949ee69 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -107,8 +107,8 @@ namespace Avalonia.FreeDesktop foreach (var i in _idsToItems.Values) { i.PropertyChanged -= OnItemPropertyChanged; - if (i.Menu != null) - ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; + if (i is NativeMenuItem nmi) + ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged -= OnMenuItemsChanged; } _idsToItems.Clear(); _itemsToIds.Clear(); @@ -129,7 +129,7 @@ namespace Avalonia.FreeDesktop if (id == 0) return (null, _menu); _idsToItems.TryGetValue(id, out var item); - return (item, item?.Menu); + return (item, (item as NativeMenuItem)?.Menu); } private int GetId(NativeMenuItemBase item) @@ -140,8 +140,8 @@ namespace Avalonia.FreeDesktop _idsToItems[id] = item; _itemsToIds[item] = id; item.PropertyChanged += OnItemPropertyChanged; - if (item.Menu != null) - ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + if (item is NativeMenuItem nmi) + ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged += OnMenuItemsChanged; return id; } @@ -281,7 +281,8 @@ namespace Avalonia.FreeDesktop 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); + + children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); } return (id, props, children); From a6e5073c6bb26b9c309c87f106275a67c9b68546 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:36:06 +0100 Subject: [PATCH 40/61] attempt to test changes on application menu --- samples/ControlCatalog/App.xaml | 21 ++++++++++++++++----- samples/ControlCatalog/App.xaml.cs | 11 +++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 726a3c915e..30bfd556c5 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -20,13 +20,24 @@ - + - - - - + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 3b758b0ba8..1eebc7e3b5 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,4 +1,6 @@ +using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -6,11 +8,20 @@ namespace ControlCatalog { public class App : Application { + private NativeMenu _recentMenu; + public override void Initialize() { AvaloniaXamlLoader.Load(this); Name = "Avalonia"; + + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; + } + + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() From 23a056f2e3a7e3299be57b824590c3cb06705612 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:47:09 +0100 Subject: [PATCH 41/61] fix clearing items when updating --- samples/ControlCatalog/App.xaml | 2 +- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 30bfd556c5..324498f94d 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -29,7 +29,7 @@ - + diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 93e0dba73b..33420f49f5 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -276,6 +276,7 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } + appMenu.Clear(); AddItemsToMenu(appMenu, menuItems); _factory.SetAppMenu(appMenu); @@ -296,6 +297,7 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } + appMenu.Clear(); AddItemsToMenu(appMenu, menuItems); avnWindow.SetMainMenu(appMenu); From 512cc4108080bcdee315bc4a24da4e1ed4a32fa5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 12:53:44 +0100 Subject: [PATCH 42/61] fix app menu test --- samples/ControlCatalog/App.xaml | 22 ++++------------------ samples/ControlCatalog/App.xaml.cs | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 324498f94d..9fbf5768b1 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -20,27 +20,13 @@ - + + - - - - - - - - - - - - - - - - - + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 1eebc7e3b5..4fc63ea054 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,7 @@ namespace ControlCatalog Name = "Avalonia"; - _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; + _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) From d0dda3e7c6fbb33402320b6d9638f1d39053f752 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 13:10:10 +0100 Subject: [PATCH 43/61] correctly handle changes on app menu --- native/Avalonia.Native/src/OSX/menu.mm | 10 ++++++++++ samples/ControlCatalog/App.xaml | 14 ++++++++++---- samples/ControlCatalog/App.xaml.cs | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 84dc80806a..10047453c2 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -217,8 +217,18 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) { auto nativeMenu = dynamic_cast(s_appMenu); + auto currentMenu = [s_appMenuItem menu]; + + [currentMenu removeItem:s_appMenuItem]; + s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + [[s_appMenuItem menu] removeItem:s_appMenuItem]; + + [currentMenu insertItem:s_appMenuItem atIndex:0]; + + //[NSApp setMenu:nativeMenu->GetNative()]; + auto appMenu = [s_appMenuItem submenu]; [appMenu addItem:[NSMenuItem separatorItem]]; diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 9fbf5768b1..914f5d5b9b 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -20,13 +20,19 @@ - - + - + + + + + + + + + - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..1eebc7e3b5 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,7 @@ namespace ControlCatalog Name = "Avalonia"; - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) From a465b3ab212f956ec05728b4f034f8a368193fe4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 13:21:24 +0100 Subject: [PATCH 44/61] make app menu declaration sensible. --- samples/ControlCatalog/App.xaml | 24 +++++++------------ samples/ControlCatalog/App.xaml.cs | 2 +- .../AvaloniaNativeMenuExporter.cs | 11 ++++++++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 914f5d5b9b..335c460b40 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -19,20 +19,14 @@ - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 1eebc7e3b5..4fc63ea054 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,7 @@ namespace ControlCatalog Name = "Avalonia"; - _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; + _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 33420f49f5..cee30a6876 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -276,8 +276,17 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } + var menuItem = new NativeMenuItem(); + + menuItem.Menu = new NativeMenu(); + + foreach(var item in menuItems) + { + menuItem.Menu.Add(item); + } + appMenu.Clear(); - AddItemsToMenu(appMenu, menuItems); + AddItemsToMenu(appMenu, new List { menuItem }); _factory.SetAppMenu(appMenu); } From 2182b91023acab8625b2d66fd6ed36b6995d83c9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 13:23:18 +0100 Subject: [PATCH 45/61] tidy appmenu code --- .../AvaloniaNativeMenuExporter.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index cee30a6876..966430a241 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -112,6 +112,7 @@ namespace Avalonia.Native if(_nativeWindow is null) { + _menu = NativeMenu.GetMenu(Application.Current); SetMenu(_menu?.Items); } else @@ -265,31 +266,26 @@ namespace Avalonia.Native menuItems = new List(); } - var menu = NativeMenu.GetMenu(Application.Current); + var appMenu = _factory.ObtainAppMenu(); - if (menu != null) + if (appMenu is null) { - var appMenu = _factory.ObtainAppMenu (); - - if (appMenu is null) - { - appMenu = _factory.CreateMenu(); - } + appMenu = _factory.CreateMenu(); + } - var menuItem = new NativeMenuItem(); + var menuItem = new NativeMenuItem(); - menuItem.Menu = new NativeMenu(); + menuItem.Menu = new NativeMenu(); - foreach(var item in menuItems) - { - menuItem.Menu.Add(item); - } + foreach (var item in menuItems) + { + menuItem.Menu.Add(item); + } - appMenu.Clear(); - AddItemsToMenu(appMenu, new List { menuItem }); + appMenu.Clear(); + AddItemsToMenu(appMenu, new List { menuItem }); - _factory.SetAppMenu(appMenu); - } + _factory.SetAppMenu(appMenu); } private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) From 7d1505e42e6f7c7705db1e830eb7c9bd28b22b33 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 13:37:12 +0100 Subject: [PATCH 46/61] tidy osx menu code --- native/Avalonia.Native/src/OSX/menu.mm | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 10047453c2..416aa12ab1 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -227,8 +227,6 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) [currentMenu insertItem:s_appMenuItem atIndex:0]; - //[NSApp setMenu:nativeMenu->GetNative()]; - auto appMenu = [s_appMenuItem submenu]; [appMenu addItem:[NSMenuItem separatorItem]]; @@ -244,14 +242,14 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) // Hide Application auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; - //hideItem.target = self; + [appMenu addItem:hideItem]; // Hide Others auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; - //hideAllOthersItem.target = self; + hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; [appMenu addItem:hideAllOthersItem]; @@ -259,7 +257,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; - //showAllItem.target = self; + [appMenu addItem:showAllItem]; [appMenu addItem:[NSMenuItem separatorItem]]; @@ -268,9 +266,6 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) auto quitItem = [[NSMenuItem alloc] init]; quitItem.title = [@"Quit " stringByAppendingString:appName]; quitItem.keyEquivalent = @"q"; - // This will remain true until synced with a QCocoaMenuItem. - // This way, we will always have a functional Quit menu item - // even if no QAction is added. quitItem.action = @selector(terminate:); [appMenu addItem:quitItem]; } From b7e9a50a42b92a7ab6541ee7d2f8d33bb74e5ba2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 14:12:16 +0100 Subject: [PATCH 47/61] OSX fix logic for setting app menu. --- .../AvaloniaNativeMenuExporter.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 966430a241..d7635ebe78 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -113,7 +113,11 @@ namespace Avalonia.Native if(_nativeWindow is null) { _menu = NativeMenu.GetMenu(Application.Current); - SetMenu(_menu?.Items); + + if(_menu != null) + { + SetMenu(_menu); + } } else { @@ -259,13 +263,8 @@ namespace Avalonia.Native } } - private void SetMenu(ICollection menuItems) + private void SetMenu(NativeMenu menu) { - if (menuItems is null) - { - menuItems = new List(); - } - var appMenu = _factory.ObtainAppMenu(); if (appMenu is null) @@ -273,15 +272,15 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } - var menuItem = new NativeMenuItem(); + var menuItem = menu.Parent; - menuItem.Menu = new NativeMenu(); - - foreach (var item in menuItems) + if(menu.Parent is null) { - menuItem.Menu.Add(item); + menuItem = new NativeMenuItem(); } + menuItem.Menu = menu; + appMenu.Clear(); AddItemsToMenu(appMenu, new List { menuItem }); From c29603f132e11c825407c6cb645a8b596e7a82dd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 14:12:29 +0100 Subject: [PATCH 48/61] [osx] fix responding to updates on app menu --- native/Avalonia.Native/src/OSX/menu.mm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 416aa12ab1..cdd6dd6c78 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -219,10 +219,18 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) auto currentMenu = [s_appMenuItem menu]; - [currentMenu removeItem:s_appMenuItem]; + if (currentMenu != nullptr) + { + [currentMenu removeItem:s_appMenuItem]; + } s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + if (currentMenu == nullptr) + { + currentMenu = [s_appMenuItem menu]; + } + [[s_appMenuItem menu] removeItem:s_appMenuItem]; [currentMenu insertItem:s_appMenuItem atIndex:0]; From e738cae5c9c6886c9b05bb3f50fa8267befb6c20 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 15:33:15 +0100 Subject: [PATCH 49/61] fix build issue --- samples/ControlCatalog.Android/MainActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 157609088f..40d001a195 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace ControlCatalog.Android { if (Avalonia.Application.Current == null) { - AppBuilder.Configure(new App()) + AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); Content = new MainView(); From 981bc5cff1eb633c5d066c4471991d4f9b12f910 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 18:07:16 +0100 Subject: [PATCH 50/61] trigger ci --- samples/ControlCatalog.Android/MainActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 40d001a195..cb2bfcd943 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace ControlCatalog.Android { if (Avalonia.Application.Current == null) { - AppBuilder.Configure() + AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); Content = new MainView(); From e3b4ba5a61dfb1265a703e4d05b2f7ae563f0aa0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 18:57:09 +0100 Subject: [PATCH 51/61] trigger ci --- samples/ControlCatalog.Android/MainActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index cb2bfcd943..40d001a195 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace ControlCatalog.Android { if (Avalonia.Application.Current == null) { - AppBuilder.Configure() + AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); Content = new MainView(); From 4df785b9d5bc0b89ae6ab7b964665d88cac3bd54 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 19:24:31 +0100 Subject: [PATCH 52/61] target monoandroid90 --- src/Android/Avalonia.Android/Avalonia.Android.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 0089ea3b8d..c170e8449c 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid80 + monoandroid90 true From 0ca7fa3786de5c1c29de7e7d862616e2c5e1b254 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 19:33:12 +0100 Subject: [PATCH 53/61] fix control catalog android --- samples/ControlCatalog.Android/ControlCatalog.Android.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index bc76a39f08..5b82e2caee 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml From 8d585947a3d9f8e9928ea2459bb3c6164883e26b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 19:34:54 +0100 Subject: [PATCH 54/61] update android test project --- .../Avalonia.AndroidTestApplication.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 1b2b205d45..2f95a6e4bd 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml From 02af14f8843c2cee867c541fb01ffe422d33ccb9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 Sep 2019 19:55:21 +0100 Subject: [PATCH 55/61] update sdk extras? --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6d12c28846..1e599211d4 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", - "MSBuild.Sdk.Extras": "1.6.65", + "MSBuild.Sdk.Extras": "2.0.46", "AggregatePackage.NuGet.Sdk" : "0.1.12" } } From d3dab89adc5352a7fa0cff338f84ea43159f972f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 28 Sep 2019 10:22:57 +0100 Subject: [PATCH 56/61] diable android builds --- dirs.proj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dirs.proj b/dirs.proj index e56320e73f..4b3b1183f0 100644 --- a/dirs.proj +++ b/dirs.proj @@ -8,7 +8,8 @@ - + + From 3c0ab70697a68483c3828bb0b62d9205850a5899 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 29 Sep 2019 18:41:20 +0100 Subject: [PATCH 57/61] minor fixes to cope with empty application menu. --- native/Avalonia.Native/src/OSX/menu.mm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index cdd6dd6c78..d9dfe36444 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -75,7 +75,10 @@ HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) HRESULT AvnAppMenuItem::SetTitle (void* utf8String) { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } return S_OK; } @@ -235,7 +238,13 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) [currentMenu insertItem:s_appMenuItem atIndex:0]; + if([s_appMenuItem submenu] == nullptr) + { + [s_appMenuItem setSubmenu:[NSMenu new]]; + } + auto appMenu = [s_appMenuItem submenu]; + [appMenu addItem:[NSMenuItem separatorItem]]; // Services item and menu From 5041f0a6df4349d1f5e501fcd188252e4f7476c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 11:57:46 +0300 Subject: [PATCH 58/61] Fixed DBusMenuExporter --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 0ab949ee69..911dc82e96 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -33,6 +33,7 @@ namespace Avalonia.FreeDesktop private NativeMenu _menu; private Dictionary _idsToItems = new Dictionary(); private Dictionary _itemsToIds = new Dictionary(); + private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; public DBusMenuExporterImpl(Connection dbus, IntPtr xid) @@ -104,12 +105,11 @@ namespace Avalonia.FreeDesktop void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) - { + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; - if (i is NativeMenuItem nmi) - ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged -= OnMenuItemsChanged; - } + foreach(var menu in _menus) + ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menus.Clear(); _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; @@ -132,6 +132,12 @@ namespace Avalonia.FreeDesktop return (item, (item as NativeMenuItem)?.Menu); } + private void EnsureSubscribed(NativeMenu menu) + { + if(menu!=null && _menus.Add(menu)) + ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; + } + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -141,7 +147,7 @@ namespace Avalonia.FreeDesktop _itemsToIds[item] = id; item.PropertyChanged += OnItemPropertyChanged; if (item is NativeMenuItem nmi) - ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged += OnMenuItemsChanged; + EnsureSubscribed(nmi.Menu); return id; } @@ -190,12 +196,15 @@ namespace Avalonia.FreeDesktop { var (it, menu) = i; - if (it is NativeMenuItem item) + if (it is NativeMenuItemSeperator) + { + if (name == "type") + return "separator"; + } + else if (it is NativeMenuItem item) { if (name == "type") { - if (item != null && item.Header == null) - return "separator"; return null; } if (name == "label") From 465273c883125861d0dcb692e3d12d8f8d70d34e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 11:58:08 +0300 Subject: [PATCH 59/61] Implemented managed menu bar presenter for native menus --- samples/ControlCatalog/MainWindow.xaml | 2 ++ samples/ControlCatalog/MainWindow.xaml.cs | 2 +- samples/ControlCatalog/Pages/MenuPage.xaml | 5 ++- src/Avalonia.Controls/NativeMenu.Export.cs | 5 ++- src/Avalonia.Controls/NativeMenu.cs | 2 +- src/Avalonia.Controls/NativeMenuBar.cs | 36 +++++++++++++++++++ .../NativeMenuItemSeperator.cs | 7 ++-- src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + .../InverseBooleanValueConverter.cs | 20 +++++++++++ .../NativeMenuBar.xaml | 25 +++++++++++++ 10 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/NativeMenuBar.cs create mode 100644 src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs create mode 100644 src/Avalonia.Themes.Default/NativeMenuBar.xaml diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 1b01fa56a8..a8fc6bb07d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -15,11 +15,13 @@ + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 819ab0655a..7b0ee897c4 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -30,7 +30,7 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); - _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index e1a5cf2c5a..868f0df6ad 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -3,8 +3,11 @@ x:Class="ControlCatalog.Pages.MenuPage"> Menu + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + A window menu - + IsNativeMenuExportedProperty = - AvaloniaProperty.RegisterAttached("IsNativeMenuExported", - defaultBindingMode: BindingMode.OneWayToSource); + AvaloniaProperty.RegisterAttached("IsNativeMenuExported"); public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); @@ -53,7 +52,7 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("NativeMenuItems", validate: + = AvaloniaProperty.RegisterAttached("Menu", validate: (o, v) => { if(!(o is Application || o is TopLevel)) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 58cff581e1..54aa2b5e3d 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls { public partial class NativeMenu : AvaloniaObject, IEnumerable { - private AvaloniaList _items = + private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; [Content] diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs new file mode 100644 index 0000000000..9b96ab9c8c --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + public class NativeMenuBar : TemplatedControl + { + public static readonly AttachedProperty EnableMenuItemClickForwardingProperty = + AvaloniaProperty.RegisterAttached( + "EnableMenuItemClickForwarding"); + + static NativeMenuBar() + { + EnableMenuItemClickForwardingProperty.Changed.Subscribe(args => + { + var item = (MenuItem)args.Sender; + if (args.NewValue.Equals(true)) + item.Click += OnMenuItemClick; + else + item.Click -= OnMenuItemClick; + }); + } + + public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) + { + menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); + } + + private static void OnMenuItemClick(object sender, RoutedEventArgs e) + { + (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs index 85d62023d4..e743483dab 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeperator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeperator.cs @@ -1,7 +1,10 @@ -namespace Avalonia.Controls +using System; + +namespace Avalonia.Controls { public class NativeMenuItemSeperator : NativeMenuItemBase { - + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; } } diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 114979fba2..67279fca99 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -51,4 +51,5 @@ + diff --git a/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs new file mode 100644 index 0000000000..7befc81b8e --- /dev/null +++ b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Themes.Default +{ + class InverseBooleanValueConverter : IValueConverter + { + public bool Default { get; set; } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : Default; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : !Default; + } + } +} diff --git a/src/Avalonia.Themes.Default/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/NativeMenuBar.xaml new file mode 100644 index 0000000000..2832bab226 --- /dev/null +++ b/src/Avalonia.Themes.Default/NativeMenuBar.xaml @@ -0,0 +1,25 @@ + + + + + + From dbcc58fa074e028a2158907f071d14a5d90e3291 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 12:00:15 +0300 Subject: [PATCH 60/61] Remove NativeMenu.PrependApplicationMenu for now --- src/Avalonia.Controls/NativeMenu.Export.cs | 14 -------------- .../Platform/ITopLevelNativeMenuExporter.cs | 1 - src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 5 ----- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 5 ----- 4 files changed, 25 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index e3e134d316..5d3a4526cc 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -62,15 +62,6 @@ namespace Avalonia.Controls 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 PrependApplicationMenuProperty - = AvaloniaProperty.RegisterAttached("PrependApplicationMenu"); - - public static void SetPrependApplicationMenu(TopLevel tl, bool value) => - tl.SetValue(PrependApplicationMenuProperty, value); - - public static bool GetPrependApplicationMenu(TopLevel tl) => tl.GetValue(PrependApplicationMenuProperty); static NativeMenu() { @@ -89,11 +80,6 @@ namespace Avalonia.Controls GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); } }); - - PrependApplicationMenuProperty.Changed.Subscribe(args => - { - GetInfo((TopLevel)args.Sender).Exporter?.SetPrependApplicationMenu((bool)args.NewValue); - }); } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 5112424c3c..3ac5f28956 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -9,7 +9,6 @@ namespace Avalonia.Controls.Platform bool IsNativeMenuExported { get; } event EventHandler OnIsNativeMenuExportedChanged; void SetNativeMenu(NativeMenu menu); - void SetPrependApplicationMenu(bool prepend); } public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 911dc82e96..90239b5a49 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -161,11 +161,6 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public void SetPrependApplicationMenu(bool prepend) - { - // Not implemented yet :( - } - public ObjectPath ObjectPath { get; } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index d7635ebe78..1a22b95409 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -83,11 +83,6 @@ namespace Avalonia.Native DoLayoutReset(); } - public void SetPrependApplicationMenu(bool prepend) - { - // OSX always exports the app menu. - } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); From f02bc61bfee4871c45b8868d4f511aba53bfadf1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 12:21:23 +0300 Subject: [PATCH 61/61] I've said remove NativeMenu.PrependApplicationMenu for now --- samples/ControlCatalog/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a8fc6bb07d..6088f2ec57 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" NativeMenu.PrependApplicationMenu="True"> + x:Class="ControlCatalog.MainWindow">