committed by
GitHub
51 changed files with 2191 additions and 73 deletions
@ -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" |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>IDEDidComputeMac32BitWarning</key> |
|||
<true/> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,80 @@ |
|||
//
|
|||
// 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
|
|||
- (void)setMenu:(NSMenu*) menu; |
|||
@end |
|||
|
|||
@interface AvnMenuItem : NSMenuItem |
|||
- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem; |
|||
- (void)didSelectItem:(id)sender; |
|||
@end |
|||
|
|||
class AvnAppMenuItem : public ComSingleObject<IAvnAppMenuItem, &IID_IAvnAppMenuItem> |
|||
{ |
|||
private: |
|||
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
|
|||
IAvnActionCallback* _callback; |
|||
IAvnPredicateCallback* _predicate; |
|||
bool _isSeperator; |
|||
|
|||
public: |
|||
FORWARD_IUNKNOWN() |
|||
|
|||
AvnAppMenuItem(bool isSeperator); |
|||
|
|||
NSMenuItem* 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<IAvnAppMenu, &IID_IAvnAppMenu> |
|||
{ |
|||
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 |
|||
|
|||
@ -0,0 +1,305 @@ |
|||
|
|||
#include "common.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(bool isSeperator) |
|||
{ |
|||
_isSeperator = isSeperator; |
|||
|
|||
if(isSeperator) |
|||
{ |
|||
_native = [NSMenuItem separatorItem]; |
|||
} |
|||
else |
|||
{ |
|||
_native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; |
|||
} |
|||
|
|||
_callback = nullptr; |
|||
} |
|||
|
|||
NSMenuItem* AvnAppMenuItem::GetNative() |
|||
{ |
|||
return _native; |
|||
} |
|||
|
|||
HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) |
|||
{ |
|||
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative(); |
|||
|
|||
[_native setSubmenu: nsMenu]; |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
HRESULT AvnAppMenuItem::SetTitle (void* utf8String) |
|||
{ |
|||
if (utf8String != nullptr) |
|||
{ |
|||
[_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<AvnAppMenuItem*>(item); |
|||
|
|||
if(avnMenuItem != nullptr) |
|||
{ |
|||
[_native addItem: avnMenuItem->GetNative()]; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) |
|||
{ |
|||
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item); |
|||
|
|||
if(avnMenuItem != nullptr) |
|||
{ |
|||
[_native removeItem:avnMenuItem->GetNative()]; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
HRESULT AvnAppMenu::SetTitle (void* utf8String) |
|||
{ |
|||
if (utf8String != nullptr) |
|||
{ |
|||
[_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
HRESULT AvnAppMenu::Clear() |
|||
{ |
|||
[_native removeAllItems]; |
|||
return S_OK; |
|||
} |
|||
|
|||
extern IAvnAppMenu* CreateAppMenu() |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
id menuBar = [NSMenu new]; |
|||
return new AvnAppMenu(menuBar); |
|||
} |
|||
} |
|||
|
|||
extern IAvnAppMenuItem* CreateAppMenuItem() |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
return new AvnAppMenuItem(false); |
|||
} |
|||
} |
|||
|
|||
extern IAvnAppMenuItem* CreateAppMenuItemSeperator() |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
return new AvnAppMenuItem(true); |
|||
} |
|||
} |
|||
|
|||
static IAvnAppMenu* s_appMenu = nullptr; |
|||
static NSMenuItem* s_appMenuItem = nullptr; |
|||
|
|||
extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) |
|||
{ |
|||
s_appMenu = menu; |
|||
|
|||
if(s_appMenu != nullptr) |
|||
{ |
|||
auto nativeMenu = dynamic_cast<AvnAppMenu*>(s_appMenu); |
|||
|
|||
auto currentMenu = [s_appMenuItem menu]; |
|||
|
|||
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]; |
|||
|
|||
if([s_appMenuItem submenu] == nullptr) |
|||
{ |
|||
[s_appMenuItem setSubmenu:[NSMenu new]]; |
|||
} |
|||
|
|||
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"]; |
|||
|
|||
[appMenu addItem:hideItem]; |
|||
|
|||
// Hide Others |
|||
auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" |
|||
action:@selector(hideOtherApplications:) |
|||
keyEquivalent:@"h"]; |
|||
|
|||
hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; |
|||
[appMenu addItem:hideAllOthersItem]; |
|||
|
|||
// Show All |
|||
auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" |
|||
action:@selector(unhideAllApplications:) |
|||
keyEquivalent:@""]; |
|||
|
|||
[appMenu addItem:showAllItem]; |
|||
|
|||
[appMenu addItem:[NSMenuItem separatorItem]]; |
|||
|
|||
// Quit Application |
|||
auto quitItem = [[NSMenuItem alloc] init]; |
|||
quitItem.title = [@"Quit " stringByAppendingString:appName]; |
|||
quitItem.keyEquivalent = @"q"; |
|||
quitItem.action = @selector(terminate:); |
|||
[appMenu addItem:quitItem]; |
|||
} |
|||
else |
|||
{ |
|||
s_appMenuItem = nullptr; |
|||
} |
|||
} |
|||
|
|||
extern IAvnAppMenu* GetAppMenu () |
|||
{ |
|||
return s_appMenu; |
|||
} |
|||
|
|||
extern NSMenuItem* GetAppMenuItem () |
|||
{ |
|||
return s_appMenuItem; |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public partial class NativeMenu |
|||
{ |
|||
public static readonly AttachedProperty<bool> IsNativeMenuExportedProperty = |
|||
AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, bool>("IsNativeMenuExported"); |
|||
|
|||
public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); |
|||
|
|||
private static readonly AttachedProperty<NativeMenuInfo> s_nativeMenuInfoProperty = |
|||
AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, NativeMenuInfo>("___NativeMenuInfo"); |
|||
|
|||
class NativeMenuInfo |
|||
{ |
|||
public bool ChangingIsExported { get; set; } |
|||
public ITopLevelNativeMenuExporter Exporter { get; } |
|||
|
|||
public NativeMenuInfo(TopLevel target) |
|||
{ |
|||
Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; |
|||
if (Exporter != null) |
|||
{ |
|||
Exporter.OnIsNativeMenuExportedChanged += delegate |
|||
{ |
|||
SetIsNativeMenuExported(target, Exporter.IsNativeMenuExported); |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
|
|||
static NativeMenuInfo GetInfo(TopLevel target) |
|||
{ |
|||
var rv = target.GetValue(s_nativeMenuInfoProperty); |
|||
if (rv == null) |
|||
{ |
|||
target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target)); |
|||
SetIsNativeMenuExported(target, rv.Exporter?.IsNativeMenuExported ?? false); |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
static void SetIsNativeMenuExported(TopLevel tl, bool value) |
|||
{ |
|||
GetInfo(tl).ChangingIsExported = true; |
|||
tl.SetValue(IsNativeMenuExportedProperty, value); |
|||
} |
|||
|
|||
public static readonly AttachedProperty<NativeMenu> MenuProperty |
|||
= AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu", 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); |
|||
|
|||
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); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
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<NativeMenuItemBase> |
|||
{ |
|||
private readonly AvaloniaList<NativeMenuItemBase> _items = |
|||
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove }; |
|||
private NativeMenuItem _parent; |
|||
[Content] |
|||
public IList<NativeMenuItemBase> Items => _items; |
|||
|
|||
public NativeMenu() |
|||
{ |
|||
_items.Validate = Validator; |
|||
_items.CollectionChanged += ItemsChanged; |
|||
} |
|||
|
|||
private void Validator(NativeMenuItemBase 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 (NativeMenuItemBase i in e.OldItems) |
|||
i.Parent = null; |
|||
if(e.NewItems!=null) |
|||
foreach (NativeMenuItemBase i in e.NewItems) |
|||
i.Parent = this; |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenu, NativeMenuItem> ParentProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenu, NativeMenuItem>("Parent", o => o.Parent, (o, v) => o.Parent = v); |
|||
|
|||
public NativeMenuItem Parent |
|||
{ |
|||
get => _parent; |
|||
set => SetAndRaise(ParentProperty, ref _parent, value); |
|||
} |
|||
|
|||
public void Add(NativeMenuItemBase item) => _items.Add(item); |
|||
|
|||
public IEnumerator<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
} |
|||
} |
|||
@ -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<bool> EnableMenuItemClickForwardingProperty = |
|||
AvaloniaProperty.RegisterAttached<NativeMenuBar, MenuItem, Boolean>( |
|||
"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(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,162 @@ |
|||
using System; |
|||
using System.Windows.Input; |
|||
using Avalonia.Input; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class NativeMenuItem : NativeMenuItemBase |
|||
{ |
|||
private string _header; |
|||
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<EventArgs> |
|||
{ |
|||
private readonly NativeMenuItem _parent; |
|||
|
|||
public CanExecuteChangedSubscriber(NativeMenuItem parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
public void OnEvent(object sender, EventArgs e) |
|||
{ |
|||
_parent.CanExecuteChanged(); |
|||
} |
|||
} |
|||
|
|||
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; |
|||
|
|||
|
|||
public NativeMenuItem() |
|||
{ |
|||
_canExecuteChangedSubscriber = new CanExecuteChangedSubscriber(this); |
|||
} |
|||
|
|||
public NativeMenuItem(string header) : this() |
|||
{ |
|||
Header = header; |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o._menu, |
|||
(o, v) => |
|||
{ |
|||
if (v.Parent != null && v.Parent != o) |
|||
throw new InvalidOperationException("NativeMenu already has a parent"); |
|||
o._menu = v; |
|||
}); |
|||
|
|||
public NativeMenu Menu |
|||
{ |
|||
get => _menu; |
|||
set |
|||
{ |
|||
if (value.Parent != null && value.Parent != this) |
|||
throw new InvalidOperationException("NativeMenu already has a parent"); |
|||
SetAndRaise(MenuProperty, ref _menu, value); |
|||
} |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, string> HeaderProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o._header, (o, v) => o._header = v); |
|||
|
|||
public string Header |
|||
{ |
|||
get => GetValue(HeaderProperty); |
|||
set => SetValue(HeaderProperty, value); |
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); |
|||
|
|||
public KeyGesture Gesture |
|||
{ |
|||
get => GetValue(GestureProperty); |
|||
set => SetValue(GestureProperty, value); |
|||
} |
|||
|
|||
private ICommand _command; |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, ICommand> CommandProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, ICommand>(nameof(Command), |
|||
o => o._command, (o, v) => |
|||
{ |
|||
if (o._command != null) |
|||
WeakSubscriptionManager.Unsubscribe(o._command, |
|||
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); |
|||
o._command = v; |
|||
if (o._command != null) |
|||
WeakSubscriptionManager.Subscribe(o._command, |
|||
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); |
|||
o.CanExecuteChanged(); |
|||
}); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommandParameter"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<object> CommandParameterProperty = |
|||
Button.CommandParameterProperty.AddOwner<MenuItem>(); |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, bool> EnabledProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(Enabled), o => o._enabled, |
|||
(o, v) => o._enabled = v, true); |
|||
|
|||
public bool Enabled |
|||
{ |
|||
get => GetValue(EnabledProperty); |
|||
set => SetValue(EnabledProperty, value); |
|||
} |
|||
|
|||
void CanExecuteChanged() |
|||
{ |
|||
Enabled = _command?.CanExecute(null) ?? true; |
|||
} |
|||
|
|||
public bool HasClickHandlers => Clicked != null; |
|||
|
|||
public ICommand Command |
|||
{ |
|||
get => GetValue(CommandProperty); |
|||
set => SetValue(CommandProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
|
|||
/// <see cref="NativeMenuItem"/>.
|
|||
/// </summary>
|
|||
public object CommandParameter |
|||
{ |
|||
get { return GetValue(CommandParameterProperty); } |
|||
set { SetValue(CommandParameterProperty, value); } |
|||
} |
|||
|
|||
public event EventHandler Clicked; |
|||
|
|||
public void RaiseClick() |
|||
{ |
|||
Clicked?.Invoke(this, new EventArgs()); |
|||
|
|||
if (Command?.CanExecute(CommandParameter) == true) |
|||
{ |
|||
Command.Execute(CommandParameter); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class NativeMenuItemBase : AvaloniaObject |
|||
{ |
|||
private NativeMenu _parent; |
|||
|
|||
internal NativeMenuItemBase() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public static readonly DirectProperty<NativeMenuItem, NativeMenu> ParentProperty = |
|||
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>("Parent", o => o.Parent, (o, v) => o.Parent = v); |
|||
|
|||
public NativeMenu Parent |
|||
{ |
|||
get => _parent; |
|||
set => SetAndRaise(ParentProperty, ref _parent, value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
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 => "-"; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
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); |
|||
} |
|||
|
|||
public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl |
|||
{ |
|||
ITopLevelNativeMenuExporter NativeMenuExporter { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Threading; |
|||
using Tmds.DBus; |
|||
|
|||
namespace Avalonia.FreeDesktop |
|||
{ |
|||
public class DBusHelper |
|||
{ |
|||
/// <summary>
|
|||
/// This class uses synchronous execution at DBus connection establishment stage
|
|||
/// then switches to using AvaloniaSynchronizationContext
|
|||
/// </summary>
|
|||
class DBusSyncContext : SynchronizationContext |
|||
{ |
|||
private SynchronizationContext _ctx; |
|||
private object _lock = new object(); |
|||
|
|||
public override void Post(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_ctx != null) |
|||
_ctx?.Post(d, state); |
|||
else |
|||
lock (_lock) |
|||
d(state); |
|||
} |
|||
} |
|||
|
|||
public override void Send(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_ctx != null) |
|||
_ctx?.Send(d, state); |
|||
else |
|||
|
|||
d(state); |
|||
} |
|||
} |
|||
|
|||
public void Initialized() |
|||
{ |
|||
lock (_lock) |
|||
_ctx = new AvaloniaSynchronizationContext(); |
|||
} |
|||
} |
|||
public static Connection Connection { get; private set; } |
|||
|
|||
public static Exception TryInitialize(string dbusAddress = null) |
|||
{ |
|||
var oldContext = SynchronizationContext.Current; |
|||
try |
|||
{ |
|||
|
|||
var dbusContext = new DBusSyncContext(); |
|||
SynchronizationContext.SetSynchronizationContext(dbusContext); |
|||
var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) |
|||
{ |
|||
AutoConnect = false, |
|||
SynchronizationContext = dbusContext |
|||
}); |
|||
// Connect synchronously
|
|||
conn.ConnectAsync().Wait(); |
|||
|
|||
// Initialize a brand new sync-context
|
|||
dbusContext.Initialized(); |
|||
Connection = conn; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
return e; |
|||
} |
|||
finally |
|||
{ |
|||
SynchronizationContext.SetSynchronizationContext(oldContext); |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading.Tasks; |
|||
using Tmds.DBus; |
|||
|
|||
[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] |
|||
namespace Avalonia.FreeDesktop.DBusMenu |
|||
{ |
|||
|
|||
[DBusInterface("org.freedesktop.DBus.Properties")] |
|||
interface IFreeDesktopDBusProperties : IDBusObject |
|||
{ |
|||
Task<object> GetAsync(string prop); |
|||
Task<DBusMenuProperties> GetAllAsync(); |
|||
Task SetAsync(string prop, object val); |
|||
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler); |
|||
} |
|||
|
|||
[DBusInterface("com.canonical.dbusmenu")] |
|||
interface IDBusMenu : IFreeDesktopDBusProperties |
|||
{ |
|||
Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); |
|||
Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); |
|||
Task<object> GetPropertyAsync(int Id, string Name); |
|||
Task EventAsync(int Id, string EventId, object Data, uint Timestamp); |
|||
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); |
|||
Task<bool> AboutToShowAsync(int Id); |
|||
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); |
|||
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError = null); |
|||
} |
|||
|
|||
[Dictionary] |
|||
class DBusMenuProperties |
|||
{ |
|||
public uint Version { get; set; } = default (uint); |
|||
public string TextDirection { get; set; } = default (string); |
|||
public string Status { get; set; } = default (string); |
|||
public string[] IconThemePath { get; set; } = default (string[]); |
|||
} |
|||
|
|||
|
|||
[DBusInterface("com.canonical.AppMenu.Registrar")] |
|||
interface IRegistrar : IDBusObject |
|||
{ |
|||
Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); |
|||
Task UnregisterWindowAsync(uint WindowId); |
|||
Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); |
|||
Task<(uint, string, ObjectPath)[]> GetMenusAsync(); |
|||
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception> onError = null); |
|||
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception> onError = null); |
|||
} |
|||
} |
|||
@ -0,0 +1,387 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Reactive.Disposables; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.FreeDesktop.DBusMenu; |
|||
using Avalonia.Input; |
|||
using Avalonia.Threading; |
|||
using Tmds.DBus; |
|||
#pragma warning disable 1998
|
|||
|
|||
namespace Avalonia.FreeDesktop |
|||
{ |
|||
public class DBusMenuExporter |
|||
{ |
|||
public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) |
|||
{ |
|||
if (DBusHelper.Connection == null) |
|||
return null; |
|||
|
|||
return new DBusMenuExporterImpl(DBusHelper.Connection, xid); |
|||
} |
|||
|
|||
class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable |
|||
{ |
|||
private readonly Connection _dbus; |
|||
private readonly uint _xid; |
|||
private IRegistrar _registar; |
|||
private bool _disposed; |
|||
private uint _revision = 1; |
|||
private NativeMenu _menu; |
|||
private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>(); |
|||
private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>(); |
|||
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>(); |
|||
private bool _resetQueued; |
|||
private int _nextId = 1; |
|||
public DBusMenuExporterImpl(Connection dbus, IntPtr xid) |
|||
{ |
|||
_dbus = dbus; |
|||
_xid = (uint)xid.ToInt32(); |
|||
ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" |
|||
+ Guid.NewGuid().ToString().Replace("-", "")); |
|||
SetNativeMenu(new NativeMenu()); |
|||
Init(); |
|||
} |
|||
|
|||
async void Init() |
|||
{ |
|||
try |
|||
{ |
|||
await _dbus.RegisterObjectAsync(this); |
|||
_registar = DBusHelper.Connection.CreateProxy<IRegistrar>( |
|||
"com.canonical.AppMenu.Registrar", |
|||
"/com/canonical/AppMenu/Registrar"); |
|||
if (!_disposed) |
|||
await _registar.RegisterWindowAsync(_xid, ObjectPath); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Console.Error.WriteLine(e); |
|||
// It's not really important if this code succeeds,
|
|||
// and it's not important to know if it succeeds
|
|||
// since even if we register the window it's not guaranteed that
|
|||
// menu will be actually exported
|
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_disposed) |
|||
return; |
|||
_disposed = true; |
|||
_dbus.UnregisterObject(this); |
|||
// Fire and forget
|
|||
_registar?.UnregisterWindowAsync(_xid); |
|||
} |
|||
|
|||
|
|||
|
|||
public bool IsNativeMenuExported { get; private set; } |
|||
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; |
|||
foreach(var menu in _menus) |
|||
((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; |
|||
_menus.Clear(); |
|||
_idsToItems.Clear(); |
|||
_itemsToIds.Clear(); |
|||
_revision++; |
|||
LayoutUpdated?.Invoke((_revision, 0)); |
|||
} |
|||
|
|||
void QueueReset() |
|||
{ |
|||
if(_resetQueued) |
|||
return; |
|||
_resetQueued = true; |
|||
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); |
|||
} |
|||
|
|||
private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) |
|||
{ |
|||
if (id == 0) |
|||
return (null, _menu); |
|||
_idsToItems.TryGetValue(id, out var item); |
|||
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)) |
|||
return id; |
|||
id = _nextId++; |
|||
_idsToItems[id] = item; |
|||
_itemsToIds[item] = id; |
|||
item.PropertyChanged += OnItemPropertyChanged; |
|||
if (item is NativeMenuItem nmi) |
|||
EnsureSubscribed(nmi.Menu); |
|||
return id; |
|||
} |
|||
|
|||
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
public ObjectPath ObjectPath { get; } |
|||
|
|||
|
|||
async Task<object> IFreeDesktopDBusProperties.GetAsync(string prop) |
|||
{ |
|||
if (prop == "Version") |
|||
return 2; |
|||
if (prop == "Status") |
|||
return "normal"; |
|||
return 0; |
|||
} |
|||
|
|||
async Task<DBusMenuProperties> IFreeDesktopDBusProperties.GetAllAsync() |
|||
{ |
|||
return new DBusMenuProperties |
|||
{ |
|||
Version = 2, |
|||
Status = "normal", |
|||
}; |
|||
} |
|||
|
|||
private static string[] AllProperties = new[] |
|||
{ |
|||
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" |
|||
}; |
|||
|
|||
object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) |
|||
{ |
|||
var (it, menu) = i; |
|||
|
|||
if (it is NativeMenuItemSeperator) |
|||
{ |
|||
if (name == "type") |
|||
return "separator"; |
|||
} |
|||
else if (it is NativeMenuItem item) |
|||
{ |
|||
if (name == "type") |
|||
{ |
|||
return null; |
|||
} |
|||
if (name == "label") |
|||
return item?.Header ?? "<null>"; |
|||
if (name == "enabled") |
|||
{ |
|||
if (item == null) |
|||
return null; |
|||
if (item.Menu != null && item.Menu.Items.Count == 0) |
|||
return false; |
|||
if (item.Enabled == false) |
|||
return false; |
|||
return null; |
|||
} |
|||
if (name == "shortcut") |
|||
{ |
|||
if (item?.Gesture == null) |
|||
return null; |
|||
if (item.Gesture.KeyModifiers == 0) |
|||
return null; |
|||
var lst = new List<string>(); |
|||
var mod = item.Gesture; |
|||
if ((mod.KeyModifiers & KeyModifiers.Control) != 0) |
|||
lst.Add("Control"); |
|||
if ((mod.KeyModifiers & KeyModifiers.Alt) != 0) |
|||
lst.Add("Alt"); |
|||
if ((mod.KeyModifiers & KeyModifiers.Shift) != 0) |
|||
lst.Add("Shift"); |
|||
if ((mod.KeyModifiers & KeyModifiers.Meta) != 0) |
|||
lst.Add("Super"); |
|||
lst.Add(item.Gesture.Key.ToString()); |
|||
return new[] { lst.ToArray() }; |
|||
} |
|||
|
|||
if (name == "children-display") |
|||
return menu != null ? "submenu" : null; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>(); |
|||
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) |
|||
{ |
|||
if (names?.Length > 0 != true) |
|||
names = AllProperties; |
|||
_reusablePropertyList.Clear(); |
|||
foreach (var n in names) |
|||
{ |
|||
var v = GetProperty(i, n); |
|||
if (v != null) |
|||
_reusablePropertyList.Add(new KeyValuePair<string, object>(n, v)); |
|||
} |
|||
|
|||
return _reusablePropertyList.ToArray(); |
|||
} |
|||
|
|||
|
|||
public Task SetAsync(string prop, object val) => Task.CompletedTask; |
|||
|
|||
public Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync( |
|||
int ParentId, int RecursionDepth, string[] PropertyNames) |
|||
{ |
|||
var menu = GetMenu(ParentId); |
|||
var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); |
|||
if (!IsNativeMenuExported) |
|||
{ |
|||
IsNativeMenuExported = true; |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); |
|||
}); |
|||
} |
|||
return Task.FromResult(rv); |
|||
} |
|||
|
|||
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase 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 as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); |
|||
} |
|||
|
|||
return (id, props, children); |
|||
} |
|||
|
|||
public Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) |
|||
{ |
|||
var arr = new (int, KeyValuePair<string, object>[])[Ids.Length]; |
|||
for (var c = 0; c < Ids.Length; c++) |
|||
{ |
|||
var id = Ids[c]; |
|||
var item = GetMenu(id); |
|||
var props = GetProperties(item, PropertyNames); |
|||
arr[c] = (id, props); |
|||
} |
|||
|
|||
return Task.FromResult(arr); |
|||
} |
|||
|
|||
public async Task<object> GetPropertyAsync(int Id, string Name) |
|||
{ |
|||
return GetProperty(GetMenu(Id), Name) ?? 0; |
|||
} |
|||
|
|||
|
|||
|
|||
public void HandleEvent(int id, string eventId, object data, uint timestamp) |
|||
{ |
|||
if (eventId == "clicked") |
|||
{ |
|||
var item = GetMenu(id).item; |
|||
|
|||
if (item is NativeMenuItem menuItem) |
|||
{ |
|||
if (menuItem?.Enabled == true) |
|||
menuItem.RaiseClick(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) |
|||
{ |
|||
HandleEvent(Id, EventId, Data, Timestamp); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) |
|||
{ |
|||
foreach (var e in Events) |
|||
HandleEvent(e.id, e.eventId, e.data, e.timestamp); |
|||
return Task.FromResult(new int[0]); |
|||
} |
|||
|
|||
public async Task<bool> AboutToShowAsync(int Id) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) |
|||
{ |
|||
return (new int[0], new int[0]); |
|||
} |
|||
|
|||
#region Events
|
|||
|
|||
private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> |
|||
ItemsPropertiesUpdated; |
|||
private event Action<(uint revision, int parent)> LayoutUpdated; |
|||
private event Action<(int id, uint timestamp)> ItemActivationRequested; |
|||
private event Action<PropertyChanges> PropertiesChanged; |
|||
|
|||
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError) |
|||
{ |
|||
ItemsPropertiesUpdated += handler; |
|||
return Disposable.Create(() => ItemsPropertiesUpdated -= handler); |
|||
} |
|||
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError) |
|||
{ |
|||
LayoutUpdated += handler; |
|||
return Disposable.Create(() => LayoutUpdated -= handler); |
|||
} |
|||
|
|||
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError) |
|||
{ |
|||
ItemActivationRequested+= handler; |
|||
return Disposable.Create(() => ItemActivationRequested -= handler); |
|||
} |
|||
|
|||
async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> handler) |
|||
{ |
|||
PropertiesChanged += handler; |
|||
return Disposable.Create(() => PropertiesChanged -= handler); |
|||
} |
|||
|
|||
#endregion
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,305 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
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<bool> _predicate; |
|||
|
|||
public PredicateCallback(Func<bool> predicate) |
|||
{ |
|||
_predicate = predicate; |
|||
} |
|||
|
|||
bool IAvnPredicateCallback.Evaluate() |
|||
{ |
|||
return _predicate(); |
|||
} |
|||
} |
|||
|
|||
class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter |
|||
{ |
|||
private IAvaloniaNativeFactory _factory; |
|||
private NativeMenu _menu; |
|||
private bool _resetQueued; |
|||
private bool _exported = false; |
|||
private IAvnWindow _nativeWindow; |
|||
private List<NativeMenuItem> _menuItems = new List<NativeMenuItem>(); |
|||
|
|||
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) |
|||
{ |
|||
_factory = factory; |
|||
_nativeWindow = nativeWindow; |
|||
|
|||
DoLayoutReset(); |
|||
} |
|||
|
|||
public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) |
|||
{ |
|||
_factory = factory; |
|||
|
|||
_menu = NativeMenu.GetMenu(Application.Current); |
|||
DoLayoutReset(); |
|||
} |
|||
|
|||
public bool IsNativeMenuExported => _exported; |
|||
|
|||
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(); |
|||
} |
|||
|
|||
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
QueueReset(); |
|||
} |
|||
|
|||
void DoLayoutReset() |
|||
{ |
|||
_resetQueued = false; |
|||
foreach (var i in _menuItems) |
|||
{ |
|||
i.PropertyChanged -= OnItemPropertyChanged; |
|||
if (i.Menu != null) |
|||
((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; |
|||
} |
|||
|
|||
_menuItems.Clear(); |
|||
|
|||
if(_nativeWindow is null) |
|||
{ |
|||
_menu = NativeMenu.GetMenu(Application.Current); |
|||
|
|||
if(_menu != null) |
|||
{ |
|||
SetMenu(_menu); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
SetMenu(_nativeWindow, _menu?.Items); |
|||
} |
|||
|
|||
_exported = true; |
|||
} |
|||
|
|||
private void QueueReset() |
|||
{ |
|||
if (_resetQueued) |
|||
return; |
|||
_resetQueued = true; |
|||
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); |
|||
} |
|||
|
|||
private IAvnAppMenu CreateSubmenu(ICollection<NativeMenuItemBase> children) |
|||
{ |
|||
var menu = _factory.CreateMenu(); |
|||
|
|||
SetChildren(menu, children); |
|||
|
|||
return menu; |
|||
} |
|||
|
|||
private void AddMenuItem(NativeMenuItem item) |
|||
{ |
|||
if (item.Menu?.Items != null) |
|||
{ |
|||
((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; |
|||
} |
|||
} |
|||
|
|||
private void SetChildren(IAvnAppMenu menu, ICollection<NativeMenuItemBase> children) |
|||
{ |
|||
foreach (var i in children) |
|||
{ |
|||
if (i is NativeMenuItem item) |
|||
{ |
|||
AddMenuItem(item); |
|||
|
|||
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 || item.HasClickHandlers) |
|||
{ |
|||
return item.Enabled; |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
else if (i is NativeMenuItemSeperator seperator) |
|||
{ |
|||
menu.AddItem(_factory.CreateMenuItemSeperator()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void AddItemsToMenu(IAvnAppMenu menu, ICollection<NativeMenuItemBase> items, bool isMainMenu = false) |
|||
{ |
|||
foreach (var i in items) |
|||
{ |
|||
if (i is NativeMenuItem item) |
|||
{ |
|||
var menuItem = _factory.CreateMenuItem(); |
|||
|
|||
AddMenuItem(item); |
|||
|
|||
menuItem.SetAction(new PredicateCallback(() => |
|||
{ |
|||
if (item.Command != null || item.HasClickHandlers) |
|||
{ |
|||
return item.Enabled; |
|||
} |
|||
|
|||
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); |
|||
} |
|||
else if(i is NativeMenuItemSeperator seperator) |
|||
{ |
|||
menu.AddItem(_factory.CreateMenuItemSeperator()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SetMenu(NativeMenu menu) |
|||
{ |
|||
var appMenu = _factory.ObtainAppMenu(); |
|||
|
|||
if (appMenu is null) |
|||
{ |
|||
appMenu = _factory.CreateMenu(); |
|||
} |
|||
|
|||
var menuItem = menu.Parent; |
|||
|
|||
if(menu.Parent is null) |
|||
{ |
|||
menuItem = new NativeMenuItem(); |
|||
} |
|||
|
|||
menuItem.Menu = menu; |
|||
|
|||
appMenu.Clear(); |
|||
AddItemsToMenu(appMenu, new List<NativeMenuItemBase> { menuItem }); |
|||
|
|||
_factory.SetAppMenu(appMenu); |
|||
} |
|||
|
|||
private void SetMenu(IAvnWindow avnWindow, ICollection<NativeMenuItemBase> menuItems) |
|||
{ |
|||
if (menuItems is null) |
|||
{ |
|||
menuItems = new List<NativeMenuItemBase>(); |
|||
} |
|||
|
|||
var appMenu = avnWindow.ObtainMainMenu(); |
|||
|
|||
if (appMenu is null) |
|||
{ |
|||
appMenu = _factory.CreateMenu(); |
|||
} |
|||
|
|||
appMenu.Clear(); |
|||
AddItemsToMenu(appMenu, menuItems); |
|||
|
|||
avnWindow.SetMainMenu(appMenu); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<Style xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:local="clr-namespace:Avalonia.Themes.Default" |
|||
Selector="NativeMenuBar"> |
|||
<Style.Resources> |
|||
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/> |
|||
</Style.Resources> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Menu |
|||
IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}" |
|||
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> |
|||
<Menu.Styles> |
|||
<Style Selector="MenuItem"> |
|||
<Setter Property="Header" Value="{Binding Header}"/> |
|||
<Setter Property="Items" Value="{Binding Menu.Items}"/> |
|||
<Setter Property="Command" Value="{Binding Command}"/> |
|||
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> |
|||
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> |
|||
</Style> |
|||
</Menu.Styles> |
|||
</Menu> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
Loading…
Reference in new issue