committed by
GitHub
51 changed files with 2191 additions and 73 deletions
@ -1,7 +1,7 @@ |
|||||
{ |
{ |
||||
"msbuild-sdks": { |
"msbuild-sdks": { |
||||
"Microsoft.Build.Traversal": "1.0.43", |
"Microsoft.Build.Traversal": "1.0.43", |
||||
"MSBuild.Sdk.Extras": "1.6.65", |
"MSBuild.Sdk.Extras": "2.0.46", |
||||
"AggregatePackage.NuGet.Sdk" : "0.1.12" |
"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