Browse Source

Merge branch 'master' into fixes/497-shared-contextmenu

pull/3751/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
126292f510
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      azure-pipelines.yml
  2. 4
      build/SkiaSharp.props
  3. 46
      native/Avalonia.Native/inc/avalonia-native.h
  4. 6
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme
  5. 3
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  6. 7
      native/Avalonia.Native/src/OSX/app.mm
  7. 10
      native/Avalonia.Native/src/OSX/common.h
  8. 25
      native/Avalonia.Native/src/OSX/main.mm
  9. 36
      native/Avalonia.Native/src/OSX/menu.h
  10. 286
      native/Avalonia.Native/src/OSX/menu.mm
  11. 4
      native/Avalonia.Native/src/OSX/platformthreading.mm
  12. 6
      native/Avalonia.Native/src/OSX/window.h
  13. 138
      native/Avalonia.Native/src/OSX/window.mm
  14. 6
      packages/Avalonia/Avalonia.csproj
  15. 2
      packages/Avalonia/Avalonia.props
  16. 22
      samples/ControlCatalog/MainWindow.xaml
  17. 1
      samples/ControlCatalog/MainWindow.xaml.cs
  18. 7
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  19. 15
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  20. 8
      scripts/ReplaceNugetCache.sh
  21. 4
      src/Avalonia.Animation/Animation.cs
  22. 9
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  23. 3
      src/Avalonia.Animation/Animators/Animator`1.cs
  24. 20
      src/Avalonia.Animation/KeyFrame.cs
  25. 349
      src/Avalonia.Animation/KeySpline.cs
  26. 2
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  27. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  28. 3
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  29. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  30. 7
      src/Avalonia.Controls/Calendar/DatePicker.cs
  31. 21
      src/Avalonia.Controls/ComboBox.cs
  32. 7
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  33. 7
      src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs
  34. 20
      src/Avalonia.Controls/NativeMenu.cs
  35. 2
      src/Avalonia.Controls/NativeMenuBar.cs
  36. 118
      src/Avalonia.Controls/NativeMenuItem.cs
  37. 5
      src/Avalonia.Controls/Platform/ISystemDialogImpl.cs
  38. 57
      src/Avalonia.Controls/Primitives/Popup.cs
  39. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  40. 39
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  41. 45
      src/Avalonia.Controls/ScrollChangedEventArgs.cs
  42. 49
      src/Avalonia.Controls/ScrollViewer.cs
  43. 24
      src/Avalonia.Controls/Shapes/Path.cs
  44. 6
      src/Avalonia.Controls/SystemDialog.cs
  45. 22
      src/Avalonia.Controls/TextBox.cs
  46. 12
      src/Avalonia.Controls/TreeViewItem.cs
  47. 110
      src/Avalonia.Controls/Window.cs
  48. 48
      src/Avalonia.Controls/WindowBase.cs
  49. 4
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  50. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  51. 2
      src/Avalonia.Dialogs/ManagedFileChooser.xaml
  52. 12
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  53. 35
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  54. 14
      src/Avalonia.Input/FocusManager.cs
  55. 6
      src/Avalonia.Input/IFocusManager.cs
  56. 2
      src/Avalonia.Input/IKeyboardDevice.cs
  57. 6
      src/Avalonia.Input/IKeyboardNavigationHandler.cs
  58. 4
      src/Avalonia.Input/KeyboardDevice.cs
  59. 8
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  60. 2
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  61. 104
      src/Avalonia.Layout/AttachedLayout.cs
  62. 45
      src/Avalonia.Layout/LayoutContextAdapter.cs
  63. 50
      src/Avalonia.Layout/LayoutHelper.cs
  64. 36
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  65. 17
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  66. 160
      src/Avalonia.Layout/NonVirtualizingStackLayout.cs
  67. 8
      src/Avalonia.Layout/StackLayout.cs
  68. 8
      src/Avalonia.Layout/UniformGridLayout.cs
  69. 42
      src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
  70. 36
      src/Avalonia.Layout/VirtualizingLayout.cs
  71. 5
      src/Avalonia.Layout/VirtualizingLayoutContext.cs
  72. 415
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  73. 176
      src/Avalonia.Native/IAvnMenu.cs
  74. 175
      src/Avalonia.Native/IAvnMenuItem.cs
  75. 2
      src/Avalonia.Native/Mappings.xml
  76. 20
      src/Avalonia.Native/MenuActionCallback.cs
  77. 147
      src/Avalonia.Native/OsxUnicodeKeys.cs
  78. 20
      src/Avalonia.Native/PredicateCallback.cs
  79. 20
      src/Avalonia.Native/SystemDialogs.cs
  80. 2
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  81. 26
      src/Avalonia.Visuals/Media/FontManager.cs
  82. 3
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  83. 16
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  84. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  85. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  86. 44
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
  87. 22
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  88. 10
      src/Avalonia.X11/X11ImmediateRendererProxy.cs
  89. 24
      src/Avalonia.X11/X11Window.cs
  90. 10
      src/Avalonia.X11/XI2Manager.cs
  91. 17
      src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs
  92. 1
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  93. 40
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  94. 67
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  95. 46
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  96. 23
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  97. 9
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  98. 116
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  99. 4
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  100. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

2
azure-pipelines.yml

@ -68,7 +68,7 @@ jobs:
inputs:
script: |
brew update
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2
displayName: 'Install Nuke'

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
<PackageReference Include="SkiaSharp" Version="1.68.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
</ItemGroup>
</Project>

46
native/Avalonia.Native/inc/avalonia-native.h

@ -1,5 +1,6 @@
#include "com.h"
#include "key.h"
#include "stddef.h"
#define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id)
@ -19,8 +20,9 @@ struct IAvnGlContext;
struct IAvnGlDisplay;
struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession;
struct IAvnAppMenu;
struct IAvnAppMenuItem;
struct IAvnMenu;
struct IAvnMenuItem;
struct IAvnMenuEvents;
enum SystemDecorations {
SystemDecorationsNone = 0,
@ -175,6 +177,13 @@ enum AvnWindowEdge
WindowEdgeSouthEast
};
enum AvnMenuItemToggleType
{
None,
CheckMark,
Radio
};
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{
public:
@ -188,11 +197,10 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0;
virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0;
virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0;
virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0;
virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0;
virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0;
virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0;
virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0;
};
AVNCOM(IAvnString, 17) : IUnknown
@ -222,8 +230,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT SetTopMost (bool value) = 0;
virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0;
virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
@ -388,10 +395,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown
virtual HRESULT GetScaling(double* ret) = 0;
};
AVNCOM(IAvnAppMenu, 17) : IUnknown
AVNCOM(IAvnMenu, 17) : IUnknown
{
virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0;
virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0;
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0;
virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0;
virtual HRESULT SetTitle (void* utf8String) = 0;
virtual HRESULT Clear () = 0;
};
@ -401,12 +408,23 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown
virtual bool Evaluate() = 0;
};
AVNCOM(IAvnAppMenuItem, 19) : IUnknown
AVNCOM(IAvnMenuItem, 19) : IUnknown
{
virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0;
virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0;
virtual HRESULT SetTitle (void* utf8String) = 0;
virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0;
virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0;
virtual HRESULT SetIsChecked (bool isChecked) = 0;
virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0;
virtual HRESULT SetIcon (void* data, size_t length) = 0;
};
AVNCOM(IAvnMenuEvents, 1A) : IUnknown
{
/**
* NeedsUpdate
*/
virtual void NeedsUpdate () = 0;
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

6
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme

@ -29,8 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -58,12 +56,10 @@
</MacroExpansion>
<CommandLineArguments>
<CommandLineArgument
argument = "bin/Debug/netcoreapp2.0/ControlCatalog.NetCore.dll"
argument = "bin/Debug/netcoreapp3.1/ControlCatalog.NetCore.dll"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

3
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@ -20,6 +20,7 @@ public:
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}
@ -94,6 +95,7 @@ public:
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}
@ -182,6 +184,7 @@ public:
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}

7
native/Avalonia.Native/src/OSX/app.mm

@ -2,7 +2,8 @@
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
@end
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
@ -14,6 +15,10 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationA
}
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
[[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]];
}
}

10
native/Avalonia.Native/src/OSX/common.h

@ -15,11 +15,11 @@ extern IAvnScreens* CreateScreens();
extern IAvnClipboard* CreateClipboard();
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnAppMenu* CreateAppMenu();
extern IAvnAppMenuItem* CreateAppMenuItem();
extern IAvnAppMenuItem* CreateAppMenuItemSeperator();
extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu);
extern IAvnAppMenu* GetAppMenu ();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator();
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp();

25
native/Avalonia.Native/src/OSX/main.mm

@ -92,12 +92,11 @@ void SetProcessName(NSString* appTitle) {
PrivateLSASN asn = ls_get_current_application_asn_func();
// Constant used by WebKit; what exactly it means is unknown.
const int magic_session_constant = -2;
OSErr err =
ls_set_application_information_item_func(magic_session_constant, asn,
ls_display_name_key,
process_name,
NULL /* optional out param */);
//LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
}
class MacOptions : public ComSingleObject<IAvnMacOptions, &IID_IAvnMacOptions>
@ -228,41 +227,29 @@ public:
return S_OK;
}
virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override
virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override
{
*ppv = ::CreateAppMenu();
*ppv = ::CreateAppMenu(cb);
return S_OK;
}
virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override
virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override
{
*ppv = ::CreateAppMenuItem();
return S_OK;
}
virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override
virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
{
*ppv = ::CreateAppMenuItemSeperator();
return S_OK;
}
virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override
virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override
{
::SetAppMenu(s_appTitle, appMenu);
return S_OK;
}
virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override
{
if(retOut == nullptr)
{
return E_POINTER;
}
*retOut = ::GetAppMenu();
return S_OK;
}
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

36
native/Avalonia.Native/src/OSX/menu.h

@ -14,8 +14,10 @@
class AvnAppMenuItem;
class AvnAppMenu;
@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain
- (void)setMenu:(NSMenu*) menu;
@interface AvnMenu : NSMenu
- (id) initWithDelegate: (NSObject<NSMenuDelegate>*) del;
- (void) setHasGlobalMenuItem: (bool) value;
- (bool) hasGlobalMenuItem;
@end
@interface AvnMenuItem : NSMenuItem
@ -23,13 +25,14 @@ class AvnAppMenu;
- (void)didSelectItem:(id)sender;
@end
class AvnAppMenuItem : public ComSingleObject<IAvnAppMenuItem, &IID_IAvnAppMenuItem>
class AvnAppMenuItem : public ComSingleObject<IAvnMenuItem, &IID_IAvnMenuItem>
{
private:
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate;
bool _isSeperator;
bool _isCheckable;
public:
FORWARD_IUNKNOWN()
@ -38,7 +41,7 @@ public:
NSMenuItem* GetNative();
virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override;
virtual HRESULT SetSubMenu (IAvnMenu* menu) override;
virtual HRESULT SetTitle (void* utf8String) override;
@ -46,29 +49,36 @@ public:
virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override;
virtual HRESULT SetIsChecked (bool isChecked) override;
virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override;
virtual HRESULT SetIcon (void* data, size_t length) override;
bool EvaluateItemEnabled();
void RaiseOnClicked();
};
class AvnAppMenu : public ComSingleObject<IAvnAppMenu, &IID_IAvnAppMenu>
class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
{
private:
AvnMenu* _native;
ComPtr<IAvnMenuEvents> _baseEvents;
public:
FORWARD_IUNKNOWN()
AvnAppMenu();
AvnAppMenu(AvnMenu* native);
AvnAppMenu(IAvnMenuEvents* events);
AvnMenu* GetNative();
virtual HRESULT AddItem (IAvnAppMenuItem* item) override;
void RaiseNeedsUpdate ();
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;
virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override;
virtual HRESULT RemoveItem (IAvnMenuItem* item) override;
virtual HRESULT SetTitle (void* utf8String) override;
@ -76,5 +86,9 @@ public:
};
@interface AvnMenuDelegate : NSObject<NSMenuDelegate>
- (id) initWithParent: (AvnAppMenu*) parent;
@end
#endif

286
native/Avalonia.Native/src/OSX/menu.mm

@ -4,6 +4,30 @@
#include "window.h"
@implementation AvnMenu
{
bool _isReparented;
NSObject<NSMenuDelegate>* _wtf;
}
- (id) initWithDelegate: (NSObject<NSMenuDelegate>*)del
{
self = [super init];
self.delegate = del;
_wtf = del;
_isReparented = false;
return self;
}
- (bool)hasGlobalMenuItem
{
return _isReparented;
}
- (void)setHasGlobalMenuItem:(bool)value
{
_isReparented = value;
}
@end
@implementation AvnMenuItem
@ -46,6 +70,7 @@
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
{
_isCheckable = false;
_isSeperator = isSeperator;
if(isSeperator)
@ -65,49 +90,134 @@ NSMenuItem* AvnAppMenuItem::GetNative()
return _native;
}
HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu)
HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu)
{
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
[_native setSubmenu: nsMenu];
return S_OK;
@autoreleasepool
{
if(menu != nullptr)
{
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
[_native setSubmenu: nsMenu];
}
else
{
[_native setSubmenu: nullptr];
}
return S_OK;
}
}
HRESULT AvnAppMenuItem::SetTitle (void* utf8String)
{
if (utf8String != nullptr)
@autoreleasepool
{
[_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
if (utf8String != nullptr)
{
[_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
}
return S_OK;
}
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;
@autoreleasepool
{
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;
@autoreleasepool
{
_predicate = predicate;
_callback = callback;
return S_OK;
}
}
HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked)
{
@autoreleasepool
{
[_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)];
return S_OK;
}
}
HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType)
{
@autoreleasepool
{
switch(toggleType)
{
case AvnMenuItemToggleType::None:
[_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]];
_isCheckable = false;
break;
case AvnMenuItemToggleType::CheckMark:
[_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]];
_isCheckable = true;
break;
case AvnMenuItemToggleType::Radio:
[_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]];
_isCheckable = true;
break;
}
return S_OK;
}
}
HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length)
{
@autoreleasepool
{
if(data != nullptr)
{
NSData *imageData = [NSData dataWithBytes:data length:length];
NSImage *image = [[NSImage alloc] initWithData:imageData];
NSSize originalSize = [image size];
NSSize size;
size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333;
auto scaleFactor = size.height / originalSize.height;
size.width = originalSize.width * scaleFactor;
[image setSize: size];
[_native setImage:image];
}
else
{
[_native setImage:nullptr];
}
return S_OK;
}
}
bool AvnAppMenuItem::EvaluateItemEnabled()
@ -130,71 +240,123 @@ void AvnAppMenuItem::RaiseOnClicked()
}
}
AvnAppMenu::AvnAppMenu()
AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events)
{
_native = [AvnMenu new];
_baseEvents = events;
id del = [[AvnMenuDelegate alloc] initWithParent: this];
_native = [[AvnMenu alloc] initWithDelegate: del];
}
AvnAppMenu::AvnAppMenu(AvnMenu* native)
{
_native = native;
}
AvnMenu* AvnAppMenu::GetNative()
{
return _native;
}
HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item)
void AvnAppMenu::RaiseNeedsUpdate()
{
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
if(avnMenuItem != nullptr)
if(_baseEvents != nullptr)
{
[_native addItem: avnMenuItem->GetNative()];
_baseEvents->NeedsUpdate();
}
return S_OK;
}
HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item)
HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
{
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
if(avnMenuItem != nullptr)
@autoreleasepool
{
[_native removeItem:avnMenuItem->GetNative()];
if([_native hasGlobalMenuItem])
{
index++;
}
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
if(avnMenuItem != nullptr)
{
[_native insertItem: avnMenuItem->GetNative() atIndex:index];
}
return S_OK;
}
}
HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item)
{
@autoreleasepool
{
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
if(avnMenuItem != nullptr)
{
[_native removeItem:avnMenuItem->GetNative()];
}
return S_OK;
}
return S_OK;
}
HRESULT AvnAppMenu::SetTitle (void* utf8String)
{
if (utf8String != nullptr)
@autoreleasepool
{
[_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
if (utf8String != nullptr)
{
[_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
}
return S_OK;
}
return S_OK;
}
HRESULT AvnAppMenu::Clear()
{
[_native removeAllItems];
return S_OK;
@autoreleasepool
{
[_native removeAllItems];
return S_OK;
}
}
@implementation AvnMenuDelegate
{
ComPtr<AvnAppMenu> _parent;
}
- (id) initWithParent:(AvnAppMenu *)parent
{
self = [super init];
_parent = parent;
return self;
}
- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel
{
if(shouldCancel)
return NO;
return YES;
}
- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu
{
return [menu numberOfItems];
}
- (void)menuNeedsUpdate:(NSMenu *)menu
{
_parent->RaiseNeedsUpdate();
}
@end
extern IAvnAppMenu* CreateAppMenu()
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb)
{
@autoreleasepool
{
id menuBar = [NSMenu new];
return new AvnAppMenu(menuBar);
return new AvnAppMenu(cb);
}
}
extern IAvnAppMenuItem* CreateAppMenuItem()
extern IAvnMenuItem* CreateAppMenuItem()
{
@autoreleasepool
{
@ -202,7 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem()
}
}
extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
extern IAvnMenuItem* CreateAppMenuItemSeperator()
{
@autoreleasepool
{
@ -210,10 +372,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
}
}
static IAvnAppMenu* s_appMenu = nullptr;
static IAvnMenu* s_appMenu = nullptr;
static NSMenuItem* s_appMenuItem = nullptr;
extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
{
s_appMenu = menu;
@ -294,7 +456,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
}
}
extern IAvnAppMenu* GetAppMenu ()
extern IAvnMenu* GetAppMenu ()
{
return s_appMenu;
}

4
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -54,9 +54,11 @@ private:
{
public:
FORWARD_IUNKNOWN()
bool Running = false;
bool Cancelled = false;
virtual void Cancel()
virtual void Cancel() override
{
Cancelled = true;
if(Running)

6
native/Avalonia.Native/src/OSX/window.h

@ -19,7 +19,11 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
-(void) applyMenu:(NSMenu *)menu;
-(bool) isModal;
-(void) setModal: (bool) isModal;
-(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;
-(double) getScaling;
@end

138
native/Avalonia.Native/src/OSX/window.mm

@ -27,7 +27,7 @@ public:
NSObject<IRenderTarget>* renderTarget;
AvnPoint lastPositionSet;
NSString* _lastTitle;
IAvnAppMenu* _mainMenu;
IAvnMenu* _mainMenu;
bool _shown;
WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
@ -234,7 +234,7 @@ public:
}
}
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override
virtual HRESULT SetMainMenu(IAvnMenu* menu) override
{
_mainMenu = menu;
@ -244,18 +244,11 @@ public:
[Window applyMenu:nsmenu];
return S_OK;
}
virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override
{
if(ret == nullptr)
if ([Window isKeyWindow])
{
return E_POINTER;
[Window showWindowMenuWithAppMenu];
}
*ret = _mainMenu;
return S_OK;
}
@ -432,6 +425,7 @@ private:
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
}
virtual HRESULT Show () override
@ -440,6 +434,9 @@ private:
{
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
[Window setModal:FALSE];
WindowBaseImpl::Show();
return SetWindowState(_lastWindowState);
@ -457,6 +454,8 @@ private:
if(cparent == nullptr)
return E_INVALIDARG;
[Window setModal:TRUE];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
@ -1151,8 +1150,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr<WindowBaseImpl> _parent;
bool _canBecomeKeyAndMain;
bool _closed;
NSMenu* _menu;
bool _isAppMenuApplied;
bool _isModal;
AvnMenu* _menu;
double _lastScaling;
}
@ -1189,32 +1188,64 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
-(void) applyMenu:(NSMenu *)menu
-(void) showWindowMenuWithAppMenu
{
if(menu == nullptr)
if(_menu != nullptr)
{
menu = [NSMenu new];
auto appMenuItem = ::GetAppMenuItem();
if(appMenuItem != nullptr)
{
auto appMenu = [appMenuItem menu];
[appMenu removeItem:appMenuItem];
[_menu insertItem:appMenuItem atIndex:0];
[_menu setHasGlobalMenuItem:true];
}
[NSApp setMenu:_menu];
}
}
-(void) showAppMenuOnly
{
auto appMenuItem = ::GetAppMenuItem();
_menu = menu;
if ([self isKeyWindow])
if(appMenuItem != nullptr)
{
auto appMenu = ::GetAppMenuItem();
auto appMenu = ::GetAppMenu();
auto nativeAppMenu = dynamic_cast<AvnAppMenu*>(appMenu);
if(appMenu != nullptr)
[[appMenuItem menu] removeItem:appMenuItem];
if(_menu != nullptr)
{
[[appMenu menu] removeItem:appMenu];
[_menu insertItem:appMenu atIndex:0];
_isAppMenuApplied = true;
[_menu setHasGlobalMenuItem:false];
}
[NSApp setMenu:menu];
[nativeAppMenu->GetNative() addItem:appMenuItem];
[NSApp setMenu:nativeAppMenu->GetNative()];
}
else
{
[NSApp setMenu:nullptr];
}
}
-(void) applyMenu:(AvnMenu *)menu
{
if(menu == nullptr)
{
menu = [AvnMenu new];
}
_menu = menu;
}
-(void) setCanBecomeKeyAndMain
{
_canBecomeKeyAndMain = true;
@ -1298,11 +1329,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
if(![ch isModal])
continue;
return FALSE;
}
return TRUE;
}
-(bool) isModal
{
return _isModal;
}
-(void) setModal: (bool) isModal
{
_isModal = isModal;
}
-(void)makeKeyWindow
{
if([self activateAppropriateChild: true])
@ -1315,23 +1360,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
if([self activateAppropriateChild: true])
{
if(_menu == nullptr)
{
_menu = [NSMenu new];
}
auto appMenu = ::GetAppMenuItem();
if(appMenu != nullptr)
{
[[appMenu menu] removeItem:appMenu];
[_menu insertItem:appMenu atIndex:0];
_isAppMenuApplied = true;
}
[NSApp setMenu:_menu];
[self showWindowMenuWithAppMenu];
_parent->BaseEvents->Activated();
[super becomeKeyWindow];
@ -1383,26 +1412,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
if(_parent)
_parent->BaseEvents->Deactivated();
auto appMenuItem = ::GetAppMenuItem();
if(appMenuItem != nullptr)
{
auto appMenu = ::GetAppMenu();
auto nativeAppMenu = dynamic_cast<AvnAppMenu*>(appMenu);
[[appMenuItem menu] removeItem:appMenuItem];
[nativeAppMenu->GetNative() addItem:appMenuItem];
[NSApp setMenu:nativeAppMenu->GetNative()];
}
else
{
[NSApp setMenu:nullptr];
}
// remove window menu items from appmenu?
[self showAppMenuOnly];
[super resignKeyWindow];
}

6
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId>
</PropertyGroup>
@ -20,8 +20,8 @@
Platform=$(Platform)" />
<ItemGroup>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp3.1/designer</PackagePath>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp2.0/designer</PackagePath>
<Visible>false</Visible>
<BuildAction>None</BuildAction>
</_PackageFiles>

2
packages/Avalonia/Avalonia.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
<AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>

22
samples/ControlCatalog/MainWindow.xaml

@ -14,9 +14,9 @@
<NativeMenuItem Header="File">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent">
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenuItem.Menu>
<NativeMenu/>
</NativeMenuItem.Menu>
@ -36,6 +36,24 @@
</NativeMenu>
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItem Header="Options">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Check Me (None)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="None"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (CheckBox)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="CheckBox"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (Radio)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="Radio"
IsChecked="{Binding IsMenuItemChecked}" />
</NativeMenu>
</NativeMenuItem.Menu>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>

1
samples/ControlCatalog/MainWindow.xaml.cs

@ -29,6 +29,7 @@ namespace ControlCatalog
DataContext = new MainWindowViewModel(_notificationArea);
_recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
var mainMenu = this.FindControl<Menu>("MainMenu");
mainMenu.AttachedToVisualTree += MenuAttached;
}

7
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
@ -34,7 +37,9 @@ namespace ControlCatalog.Pages
new OpenFileDialog()
{
Title = "Open file",
Filters = GetFilters()
Filters = GetFilters(),
// Almost guaranteed to exist
InitialFileName = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("SaveFile").Click += delegate

15
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels
{
private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true;
public MainWindowViewModel(IManagedNotificationManager notificationManager)
{
_notificationManager = notificationManager;
@ -42,6 +44,11 @@ namespace ControlCatalog.ViewModels
{
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
});
ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
{
IsMenuItemChecked = !IsMenuItemChecked;
});
}
public IManagedNotificationManager NotificationManager
@ -50,6 +57,12 @@ namespace ControlCatalog.ViewModels
set { this.RaiseAndSetIfChanged(ref _notificationManager, value); }
}
public bool IsMenuItemChecked
{
get { return _isMenuItemChecked; }
set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
}
public ReactiveCommand<Unit, Unit> ShowCustomManagedNotificationCommand { get; }
public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; }
@ -59,5 +72,7 @@ namespace ControlCatalog.ViewModels
public ReactiveCommand<Unit, Unit> AboutCommand { get; }
public ReactiveCommand<Unit, Unit> ExitCommand { get; }
public ReactiveCommand<Unit, Unit> ToggleMenuItemCheckedCommand { get; }
}
}

8
scripts/ReplaceNugetCache.sh

@ -1,8 +1,8 @@
#!/usr/bin/env bash
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp3.1/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

4
src/Avalonia.Animation/Animation.cs

@ -251,10 +251,10 @@ namespace Avalonia.Animation
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{
cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
}
var newKF = new AnimatorKeyFrame(handler, cue);
var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
subscriptions.Add(newKF.BindSetter(setter, control));

9
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -24,11 +24,20 @@ namespace Avalonia.Animation
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = keySpline;
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Cue Cue { get; }
public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;

3
src/Avalonia.Animation/Animators/Animator`1.cs

@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators
else
newValue = (T)lastKeyframe.Value;
if (lastKeyframe.KeySpline != null)
progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
return Interpolate(progress, oldValue, newValue);
}

20
src/Avalonia.Animation/KeyFrame.cs

@ -19,6 +19,7 @@ namespace Avalonia.Animation
{
private TimeSpan _ktimeSpan;
private Cue _kCue;
private KeySpline _kKeySpline;
public KeyFrame()
{
@ -74,6 +75,25 @@ namespace Avalonia.Animation
}
}
/// <summary>
/// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
{
get
{
return _kKeySpline;
}
set
{
_kKeySpline = value;
if (value != null && !value.IsValid())
{
throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0.");
}
}
}
}

349
src/Avalonia.Animation/KeySpline.cs

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Avalonia;
using Avalonia.Utilities;
// Ported from WPF open-source code.
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
namespace Avalonia.Animation
{
/// <summary>
/// Determines how an animation is used based on a cubic bezier curve.
/// X1 and X2 must be between 0.0 and 1.0, inclusive.
/// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
/// </summary>
[TypeConverter(typeof(KeySplineTypeConverter))]
public class KeySpline : AvaloniaObject
{
// Control points
private double _controlPointX1;
private double _controlPointY1;
private double _controlPointX2;
private double _controlPointY2;
private bool _isSpecified;
private bool _isDirty;
// The parameter that corresponds to the most recent time
private double _parameter;
// Cached coefficients
private double _Bx; // 3*points[0].X
private double _Cx; // 3*points[1].X
private double _Cx_Bx; // 2*(Cx - Bx)
private double _three_Cx; // 3 - Cx
private double _By; // 3*points[0].Y
private double _Cy; // 3*points[1].Y
// constants
private const double _accuracy = .001; // 1/3 the desired accuracy in X
private const double _fuzz = .000001; // computational zero
/// <summary>
/// Create a <see cref="KeySpline"/> with X1 = Y1 = 0 and X2 = Y2 = 1.
/// </summary>
public KeySpline()
{
_controlPointX1 = 0.0;
_controlPointY1 = 0.0;
_controlPointX2 = 1.0;
_controlPointY2 = 1.0;
_isDirty = true;
}
/// <summary>
/// Create a <see cref="KeySpline"/> with the given parameters
/// </summary>
/// <param name="x1">X coordinate for the first control point</param>
/// <param name="y1">Y coordinate for the first control point</param>
/// <param name="x2">X coordinate for the second control point</param>
/// <param name="y2">Y coordinate for the second control point</param>
public KeySpline(double x1, double y1, double x2, double y2)
{
_controlPointX1 = x1;
_controlPointY1 = y1;
_controlPointX2 = x2;
_controlPointY2 = y2;
_isDirty = true;
}
/// <summary>
/// Parse a <see cref="KeySpline"/> from a string. The string
/// needs to contain 4 values in it for the 2 control points.
/// </summary>
/// <param name="value">string with 4 values in it</param>
/// <param name="culture">culture of the string</param>
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
public static KeySpline Parse(string value, CultureInfo culture)
{
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
}
/// <summary>
/// X coordinate of the first control point
/// </summary>
public double ControlPointX1
{
get => _controlPointX1;
set
{
if (IsValidXValue(value))
{
_controlPointX1 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the first control point
/// </summary>
public double ControlPointY1
{
get => _controlPointY1;
set => _controlPointY1 = value;
}
/// <summary>
/// X coordinate of the second control point
/// </summary>
public double ControlPointX2
{
get => _controlPointX2;
set
{
if (IsValidXValue(value))
{
_controlPointX2 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the second control point
/// </summary>
public double ControlPointY2
{
get => _controlPointY2;
set => _controlPointY2 = value;
}
/// <summary>
/// Calculates spline progress from a linear progress.
/// </summary>
/// <param name="linearProgress">the linear progress</param>
/// <returns>the spline progress</returns>
public double GetSplineProgress(double linearProgress)
{
if (_isDirty)
{
Build();
}
if (!_isSpecified)
{
return linearProgress;
}
else
{
SetParameterFromX(linearProgress);
return GetBezierValue(_By, _Cy, _parameter);
}
}
/// <summary>
/// Check to see whether the <see cref="KeySpline"/> is valid by looking
/// at its X values.
/// </summary>
/// <returns>true if the X values for this <see cref="KeySpline"/> fall in
/// acceptable range; false otherwise.</returns>
public bool IsValid()
{
return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2);
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private bool IsValidXValue(double value)
{
return value >= 0.0 && value <= 1.0;
}
/// <summary>
/// Compute cached coefficients.
/// </summary>
private void Build()
{
if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1)
{
// This KeySpline would have no effect on the progress.
_isSpecified = false;
}
else
{
_isSpecified = true;
_parameter = 0;
// X coefficients
_Bx = 3 * _controlPointX1;
_Cx = 3 * _controlPointX2;
_Cx_Bx = 2 * (_Cx - _Bx);
_three_Cx = 3 - _Cx;
// Y coefficients
_By = 3 * _controlPointY1;
_Cy = 3 * _controlPointY2;
}
_isDirty = false;
}
/// <summary>
/// Get an X or Y value with the Bezier formula.
/// </summary>
/// <param name="b">the second Bezier coefficient</param>
/// <param name="c">the third Bezier coefficient</param>
/// <param name="t">the parameter value to evaluate at</param>
/// <returns>the value of the Bezier function at the given parameter</returns>
static private double GetBezierValue(double b, double c, double t)
{
double s = 1.0 - t;
double t2 = t * t;
return b * t * s * s + c * t2 * s + t2 * t;
}
/// <summary>
/// Get X and dX/dt at a given parameter
/// </summary>
/// <param name="t">the parameter value to evaluate at</param>
/// <param name="x">the value of x there</param>
/// <param name="dx">the value of dx/dt there</param>
private void GetXAndDx(double t, out double x, out double dx)
{
double s = 1.0 - t;
double t2 = t * t;
double s2 = s * s;
x = _Bx * t * s2 + _Cx * t2 * s + t2 * t;
dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2;
}
/// <summary>
/// Compute the parameter value that corresponds to a given X value, using a modified
/// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make
/// use of some known properties of this particular function:
/// * We are only interested in solutions in the interval [0,1]
/// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use
/// that to clamp down the search interval with every probe.
/// * The derivative of X and Y are between 0 and 3.
/// </summary>
/// <param name="time">the time, scaled to fit in [0,1]</param>
private void SetParameterFromX(double time)
{
// Dynamic search interval to clamp with
double bottom = 0;
double top = 1;
if (time == 0)
{
_parameter = 0;
}
else if (time == 1)
{
_parameter = 1;
}
else
{
// Loop while improving the guess
while (top - bottom > _fuzz)
{
double x, dx, absdx;
// Get x and dx/dt at the current parameter
GetXAndDx(_parameter, out x, out dx);
absdx = Math.Abs(dx);
// Clamp down the search interval, relying on the monotonicity of X(t)
if (x > time)
{
top = _parameter; // because parameter > solution
}
else
{
bottom = _parameter; // because parameter < solution
}
// The desired accuracy is in ultimately in y, not in x, so the
// accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
// But dy/dt <=3, so we omit that
if (Math.Abs(x - time) < _accuracy * absdx)
{
break; // We're there
}
if (absdx > _fuzz)
{
// Nonzero derivative, use Newton-Raphson to obtain the next guess
double next = _parameter - (x - time) / dx;
// If next guess is out of the search interval then clamp it in
if (next >= top)
{
_parameter = (_parameter + top) / 2;
}
else if (next <= bottom)
{
_parameter = (_parameter + bottom) / 2;
}
else
{
// Next guess is inside the search interval, accept it
_parameter = next;
}
}
else // Zero derivative, halve the search interval
{
_parameter = (bottom + top) / 2;
}
}
}
}
}
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

2
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -17,7 +17,7 @@ namespace Avalonia.Threading
SystemIdle = 1,
/// <summary>
/// The job will be processed when the application sis idle.
/// The job will be processed when the application is idle.
/// </summary>
ApplicationIdle = 2,

2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@ -55,7 +55,7 @@ namespace Avalonia.Controls
binding.Mode = BindingMode.TwoWay;
}
if (binding.Converter == null)
if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat))
{
binding.Converter = DataGridValueConverter.Instance;
}

3
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@ -269,6 +269,9 @@ namespace Avalonia.Controls.Primitives
// Since we didn't know the final widths of the columns until we resized,
// we waited until now to measure each cell
double leftEdge = 0;
if (autoSizeHeight)
DesiredHeight = 0;
foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
{
DataGridCell cell = OwningRow.Cells[column.Index];

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void DropDownPopup_Closed(object sender, EventArgs e)
private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@ -1638,6 +1638,11 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{

7
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -895,12 +895,17 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
private void PopUp_Closed(object sender, EventArgs e)
private void PopUp_Closed(object sender, PopupClosedEventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}

21
src/Avalonia.Controls/ComboBox.cs

@ -234,6 +234,23 @@ namespace Avalonia.Controls
base.OnTemplateApplied(e);
}
/// <summary>
/// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
/// that caused the popup to close.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method can be overridden to control whether the event that caused the popup to close
/// is swallowed or passed through.
/// </remarks>
protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
}
internal void ItemFocused(ComboBoxItem dropDownItem)
{
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@ -242,11 +259,13 @@ namespace Avalonia.Controls
}
}
private void PopupClosed(object sender, EventArgs e)
private void PopupClosed(object sender, PopupClosedEventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
PopupClosedOverride(e);
if (CanFocus(this))
{
Focus();

7
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -0,0 +1,7 @@
namespace Avalonia.Controls
{
public interface INativeMenuExporterEventsImplBridge
{
void RaiseNeedsUpdate ();
}
}

7
src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs

@ -0,0 +1,7 @@
namespace Avalonia.Controls
{
public interface INativeMenuItemExporterEventsImplBridge
{
void RaiseClicked ();
}
}

20
src/Avalonia.Controls/NativeMenu.cs

@ -3,13 +3,11 @@ 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>
public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItemBase>, INativeMenuExporterEventsImplBridge
{
private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
@ -17,12 +15,22 @@ namespace Avalonia.Controls
[Content]
public IList<NativeMenuItemBase> Items => _items;
/// <summary>
/// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
/// </summary>
public event EventHandler<EventArgs> Opening;
public NativeMenu()
{
_items.Validate = Validator;
_items.CollectionChanged += ItemsChanged;
}
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
Opening?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj)
{
if (obj.Parent != null)
@ -31,10 +39,10 @@ namespace Avalonia.Controls
private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.OldItems!=null)
if (e.OldItems != null)
foreach (NativeMenuItemBase i in e.OldItems)
i.Parent = null;
if(e.NewItems!=null)
if (e.NewItems != null)
foreach (NativeMenuItemBase i in e.NewItems)
i.Parent = this;
}
@ -49,7 +57,7 @@ namespace Avalonia.Controls
}
public void Add(NativeMenuItemBase item) => _items.Add(item);
public IEnumerator<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()

2
src/Avalonia.Controls/NativeMenuBar.cs

@ -30,7 +30,7 @@ namespace Avalonia.Controls
private static void OnMenuItemClick(object sender, RoutedEventArgs e)
{
(((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick();
(((MenuItem)sender).DataContext as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked();
}
}
}

118
src/Avalonia.Controls/NativeMenuItem.cs

@ -1,15 +1,20 @@
using System;
using System.Windows.Input;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
public class NativeMenuItem : NativeMenuItemBase
public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge
{
private string _header;
private KeyGesture _gesture;
private bool _enabled = true;
private bool _isEnabled = true;
private ICommand _command;
private bool _isChecked = false;
private NativeMenuItemToggleType _toggleType;
private IBitmap _icon;
private NativeMenu _menu;
@ -55,13 +60,7 @@ namespace Avalonia.Controls
}
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;
});
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v);
public NativeMenu Menu
{
@ -74,39 +73,63 @@ namespace Avalonia.Controls
}
}
public static readonly DirectProperty<NativeMenuItem, IBitmap> IconProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, IBitmap>(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v);
public IBitmap Icon
{
get => _icon;
set => SetAndRaise(IconProperty, ref _icon, value);
}
public static readonly DirectProperty<NativeMenuItem, string> HeaderProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o._header, (o, v) => o._header = v);
AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o.Header, (o, v) => o.Header = v);
public string Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
}
public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v);
AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v);
public KeyGesture Gesture
{
get => GetValue(GestureProperty);
set => SetValue(GestureProperty, value);
get => _gesture;
set => SetAndRaise(GestureProperty, ref _gesture, value);
}
private ICommand _command;
public static readonly DirectProperty<NativeMenuItem, bool> IsCheckedProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(
nameof(IsChecked),
o => o.IsChecked,
(o, v) => o.IsChecked = v);
public bool IsChecked
{
get => _isChecked;
set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
}
public static readonly DirectProperty<NativeMenuItem, NativeMenuItemToggleType> ToggleTypeProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenuItemToggleType>(
nameof(ToggleType),
o => o.ToggleType,
(o, v) => o.ToggleType = v);
public NativeMenuItemToggleType ToggleType
{
get => _toggleType;
set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value);
}
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();
});
Button.CommandProperty.AddOwner<NativeMenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
enableDataValidation: true);
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
@ -114,27 +137,39 @@ namespace Avalonia.Controls
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 static readonly DirectProperty<NativeMenuItem, bool> IsEnabledProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true);
public bool Enabled
public bool IsEnabled
{
get => GetValue(EnabledProperty);
set => SetValue(EnabledProperty, value);
get => _isEnabled;
set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value);
}
void CanExecuteChanged()
{
Enabled = _command?.CanExecute(null) ?? true;
IsEnabled = _command?.CanExecute(null) ?? true;
}
public bool HasClickHandlers => Clicked != null;
public ICommand Command
{
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
get => _command;
set
{
if (_command != null)
WeakSubscriptionManager.Unsubscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
SetAndRaise(CommandProperty, ref _command, value);
if (_command != null)
WeakSubscriptionManager.Subscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
CanExecuteChanged();
}
}
/// <summary>
@ -149,7 +184,7 @@ namespace Avalonia.Controls
public event EventHandler Clicked;
public void RaiseClick()
void INativeMenuItemExporterEventsImplBridge.RaiseClicked()
{
Clicked?.Invoke(this, new EventArgs());
@ -159,4 +194,11 @@ namespace Avalonia.Controls
}
}
}
public enum NativeMenuItemToggleType
{
None,
CheckBox,
Radio
}
}

5
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Controls.Platform
{
@ -14,8 +13,8 @@ namespace Avalonia.Controls.Platform
/// <param name="dialog">The details of the file dialog to show.</param>
/// <param name="parent">The parent window.</param>
/// <returns>A task returning the selected filenames.</returns>
Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent);
Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent);
Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent);
Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent);
}
}

57
src/Avalonia.Controls/Primitives/Popup.cs

@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler? Closed;
public event EventHandler<PopupClosedEventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Closes the popup.
/// </summary>
public void Close()
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
public void Close() => CloseCore(null);
/// <summary>
/// Measures the control.
@ -389,22 +368,44 @@ namespace Avalonia.Controls.Primitives
}
}
private void CloseCore(EventArgs? closeEvent)
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Close();
CloseCore(e);
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
{
Close();
e.Handled = true;
CloseCore(e);
}
}

33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Holds data for the <see cref="Popup.Closed"/> event.
/// </summary>
public class PopupClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
/// </summary>
/// <param name="closeEvent"></param>
public PopupClosedEventArgs(EventArgs? closeEvent)
{
CloseEvent = closeEvent;
}
/// <summary>
/// Gets the event that closed the popup, if any.
/// </summary>
/// <remarks>
/// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
/// interaction that caused the popup to close if the close was caused by e.g. a pointer press
/// outside the popup. It can be used to mark the event as handled if the event should not
/// be propagated.
/// </remarks>
public EventArgs? CloseEvent { get; }
}
}

39
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -117,20 +117,41 @@ namespace Avalonia.Controls.Primitives
});
}
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
protected override Size MeasureOverride(Size availableSize)
{
var measured = base.MeasureOverride(availableSize);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;
var heightCache = Height;
if (!double.IsNaN(widthCache))
{
width = widthCache;
}
width = Math.Min(width, MaxWidth);
width = Math.Max(width, MinWidth);
if (!double.IsNaN(heightCache))
{
height = heightCache;
}
height = Math.Min(height, MaxHeight);
height = Math.Max(height, MinHeight);
return new Size(width, height);
}
protected override sealed Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
_positionerParameters.Size = finalSize;
_positionerParameters.Size = size;
UpdatePosition();
return ClientSize;
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
}
}

45
src/Avalonia.Controls/ScrollChangedEventArgs.cs

@ -0,0 +1,45 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Describes a change in scrolling state.
/// </summary>
public class ScrollChangedEventArgs : RoutedEventArgs
{
public ScrollChangedEventArgs(
Vector extentDelta,
Vector offsetDelta,
Vector viewportDelta)
: this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta)
{
}
public ScrollChangedEventArgs(
RoutedEvent routedEvent,
Vector extentDelta,
Vector offsetDelta,
Vector viewportDelta)
: base(routedEvent)
{
ExtentDelta = extentDelta;
OffsetDelta = offsetDelta;
ViewportDelta = viewportDelta;
}
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Extent"/>.
/// </summary>
public Vector ExtentDelta { get; }
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Offset"/>.
/// </summary>
public Vector OffsetDelta { get; }
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Viewport"/>.
/// </summary>
public Vector ViewportDelta { get; }
}
}

49
src/Avalonia.Controls/ScrollViewer.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -165,6 +166,14 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
public static readonly RoutedEvent<ScrollChangedEventArgs> ScrollChangedEvent =
RoutedEvent.Register<ScrollViewer, ScrollChangedEventArgs>(
nameof(ScrollChanged),
RoutingStrategies.Bubble);
internal const double DefaultSmallChange = 16;
private IDisposable _childSubscription;
@ -191,6 +200,15 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Occurs when changes are detected to the scroll position, extent, or viewport size.
/// </summary>
public event EventHandler<ScrollChangedEventArgs> ScrollChanged
{
add => AddHandler(ScrollChangedEvent, value);
remove => RemoveHandler(ScrollChangedEvent, value);
}
/// <summary>
/// Gets the extent of the scrollable content.
/// </summary>
@ -203,9 +221,11 @@ namespace Avalonia.Controls
private set
{
var old = _extent;
if (SetAndRaise(ExtentProperty, ref _extent, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(extentDelta: value - old);
}
}
}
@ -222,11 +242,13 @@ namespace Avalonia.Controls
set
{
var old = _offset;
value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(offsetDelta: value - old);
}
}
}
@ -243,9 +265,11 @@ namespace Avalonia.Controls
private set
{
var old = _viewport;
if (SetAndRaise(ViewportProperty, ref _viewport, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(viewportDelta: value - old);
}
}
}
@ -525,7 +549,10 @@ namespace Avalonia.Controls
}
}
private void CalculatedPropertiesChanged()
private void CalculatedPropertiesChanged(
Size extentDelta = default,
Vector offsetDelta = default,
Size viewportDelta = default)
{
// Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properies.
@ -546,6 +573,20 @@ namespace Avalonia.Controls
SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
}
if (extentDelta != default || offsetDelta != default || viewportDelta != default)
{
using var route = BuildEventRoute(ScrollChangedEvent);
if (route.HasHandlers)
{
var e = new ScrollChangedEventArgs(
new Vector(extentDelta.Width, extentDelta.Height),
offsetDelta,
new Vector(viewportDelta.Width, viewportDelta.Height));
route.RaiseEvent(this, e);
}
}
}
protected override void OnKeyDown(KeyEventArgs e)

24
src/Avalonia.Controls/Shapes/Path.cs

@ -1,3 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Shapes
@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
static Path()
{
AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
}
public Geometry Data
@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
}
protected override Geometry CreateDefiningGeometry() => Data;
private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldGeometry = (Geometry)e.OldValue;
var newGeometry = (Geometry)e.NewValue;
if (oldGeometry is object)
{
oldGeometry.Changed -= GeometryChanged;
}
if (newGeometry is object)
{
newGeometry.Changed += GeometryChanged;
}
}
private void GeometryChanged(object sender, EventArgs e)
{
InvalidateGeometry();
}
}
}

6
src/Avalonia.Controls/SystemDialog.cs

@ -32,7 +32,7 @@ namespace Avalonia.Controls
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
.ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
.ShowFileDialogAsync(this, parent)) ??
Array.Empty<string>()).FirstOrDefault();
}
}
@ -45,7 +45,7 @@ namespace Avalonia.Controls
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent?.PlatformImpl);
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent);
}
}
@ -61,7 +61,7 @@ namespace Avalonia.Controls
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent?.PlatformImpl);
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent);
}
}

22
src/Avalonia.Controls/TextBox.cs

@ -277,13 +277,15 @@ namespace Avalonia.Controls
get { return GetSelection(); }
set
{
if (value == null)
_undoRedoHelper.Snapshot();
if (string.IsNullOrEmpty(value))
{
return;
DeleteSelection();
}
_undoRedoHelper.Snapshot();
HandleTextInput(value);
else
{
HandleTextInput(value);
}
_undoRedoHelper.Snapshot();
}
}
@ -471,8 +473,10 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
}
handled = true;
@ -598,6 +602,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@ -621,11 +626,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
}
_undoRedoHelper.Snapshot();
handled = true;
break;
case Key.Delete:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@ -647,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
_undoRedoHelper.Snapshot();
handled = true;
break;
@ -654,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
_undoRedoHelper.Snapshot();
HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
handled = true;
}
@ -663,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
_undoRedoHelper.Snapshot();
HandleTextInput("\t");
_undoRedoHelper.Snapshot();
handled = true;
}
else

12
src/Avalonia.Controls/TreeViewItem.cs

@ -51,6 +51,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
}
@ -179,5 +180,16 @@ namespace Avalonia.Controls
return logical != null ? result : @default;
}
private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)
{
// If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be
// called when the item is removed. This results in the item not being removed from the index,
// causing #3551. In this case, update the index when Parent is changed to null.
ItemContainerGenerator.UpdateIndex();
}
}
}
}

110
src/Avalonia.Controls/Window.cs

@ -313,22 +313,7 @@ namespace Avalonia.Controls
/// Should be called from left mouse button press event handler
/// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
@ -450,6 +435,19 @@ namespace Avalonia.Controls
EnsureInitialized();
IsVisible = true;
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (initialSize != ClientSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(initialSize);
}
}
LayoutManager.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing())
@ -486,22 +484,12 @@ namespace Avalonia.Controls
/// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns>
public Task<TResult> ShowDialog<TResult>(Window owner) => ShowDialog<TResult>(owner.PlatformImpl);
/// <summary>
/// Shows the window as a dialog.
/// </summary>
/// <typeparam name="TResult">
/// The type of the result produced by the dialog.
/// </typeparam>
/// <param name="owner">The dialog's owner window.</param>
/// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns>
public Task<TResult> ShowDialog<TResult>(IWindowImpl owner)
public Task<TResult> ShowDialog<TResult>(Window owner)
{
if (owner == null)
{
throw new ArgumentNullException(nameof(owner));
}
if (IsVisible)
{
@ -512,29 +500,44 @@ namespace Avalonia.Controls
EnsureInitialized();
IsVisible = true;
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (initialSize != ClientSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(initialSize);
}
}
LayoutManager.ExecuteInitialLayoutPass(this);
var result = new TaskCompletionSource<TResult>();
using (BeginAutoSizing())
{
PlatformImpl?.ShowDialog(owner);
PlatformImpl?.ShowDialog(owner.PlatformImpl);
Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x,
x => this.Closed -= x)
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
OnOpened(EventArgs.Empty);
}
SetWindowStartupLocation(owner);
SetWindowStartupLocation(owner.PlatformImpl);
return result.Task;
}
@ -569,38 +572,60 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = availableSize;
var constraint = clientSize;
if ((sizeToContent & SizeToContent.Width) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
constraint = constraint.WithWidth(double.PositiveInfinity);
}
if ((sizeToContent & SizeToContent.Height) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
constraint = constraint.WithHeight(double.PositiveInfinity);
}
var result = base.MeasureOverride(constraint);
if ((sizeToContent & SizeToContent.Width) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
{
result = result.WithWidth(clientSize.Width);
if (!double.IsInfinity(availableSize.Width))
{
result = result.WithWidth(availableSize.Width);
}
else
{
result = result.WithWidth(clientSize.Width);
}
}
if ((sizeToContent & SizeToContent.Height) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
{
result = result.WithHeight(clientSize.Height);
if (!double.IsInfinity(availableSize.Height))
{
result = result.WithHeight(availableSize.Height);
}
else
{
result = result.WithHeight(clientSize.Height);
}
}
return result;
}
protected sealed override Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(size);
return ClientSize;
}
}
protected sealed override void HandleClosed()
{
RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
@ -616,6 +641,9 @@ namespace Avalonia.Controls
SizeToContent = SizeToContent.Manual;
}
Width = clientSize.Width;
Height = clientSize.Height;
base.HandleResized(clientSize);
}

48
src/Avalonia.Controls/WindowBase.cs

@ -224,16 +224,54 @@ namespace Avalonia.Controls
/// <param name="clientSize">The new client size.</param>
protected override void HandleResized(Size clientSize)
{
if (!AutoSizing)
{
Width = clientSize.Width;
Height = clientSize.Height;
}
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize);
}
/// <summary>
/// Overrides the core measure logic for windows.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The measured size.</returns>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// MeasureCore (such as margins and alignment) make no sense.
/// </remarks>
protected override Size MeasureCore(Size availableSize)
{
ApplyStyling();
ApplyTemplate();
var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize);
return MeasureOverride(constraint);
}
/// <summary>
/// Overrides the core arrange logic for windows.
/// </summary>
/// <param name="finalRect">The final arrange rect.</param>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// ArrangeCore (such as margins and alignment) make no sense.
/// </remarks>
protected override void ArrangeCore(Rect finalRect)
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
Bounds = new Rect(arrangeSize);
}
/// <summary>
/// Called durung the arrange pass to set the size of the window.
/// </summary>
/// <param name="size">The requested size of the window.</param>
/// <returns>The actual size of the window.</returns>
protected virtual Size ArrangeSetBounds(Size size) => size;
/// <summary>
/// Handles a window position change notification from
/// <see cref="IWindowBaseImpl.PositionChanged"/>.

4
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -166,10 +166,10 @@ namespace Avalonia.DesignerSupport.Remote
class SystemDialogsStub : ISystemDialogImpl
{
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) =>
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
Task.FromResult((string[])null);
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) =>
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
Task.FromResult((string)null);
}

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

2
src/Avalonia.Dialogs/ManagedFileChooser.xaml

@ -58,7 +58,7 @@
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin">4</Setter>
<Setter Property="Margin" Value="4"/>
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}">OK</Button>

12
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,19 +1,15 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.Platform;
namespace Avalonia.Dialogs
{
public static class ManagedFileDialogExtensions
{
class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
private class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
{
async Task<string[]> Show(SystemDialog d, IWindowImpl parent)
async Task<string[]> Show(SystemDialog d, Window parent)
{
var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
@ -39,12 +35,12 @@ namespace Avalonia.Dialogs
return result;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
return await Show(dialog, parent);
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
return (await Show(dialog, parent))?.FirstOrDefault();
}

35
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Controls;
@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop
private static string[] AllProperties = new[]
{
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display"
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
};
object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
@ -210,7 +211,7 @@ namespace Avalonia.FreeDesktop
return null;
if (item.Menu != null && item.Menu.Items.Count == 0)
return false;
if (item.Enabled == false)
if (item.IsEnabled == false)
return false;
return null;
}
@ -234,6 +235,30 @@ namespace Avalonia.FreeDesktop
return new[] { lst.ToArray() };
}
if (name == "toggle-type")
{
if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
return "checkmark";
if (item.ToggleType == NativeMenuItemToggleType.Radio)
return "radio";
}
if (name == "toggle-state")
{
if (item.ToggleType != NativeMenuItemToggleType.None)
return item.IsChecked ? 1 : 0;
}
if (name == "icon-data")
{
if (item.Icon != null)
{
var ms = new MemoryStream();
item.Icon.Save(ms);
return ms.ToArray();
}
}
if (name == "children-display")
return menu != null ? "submenu" : null;
}
@ -319,10 +344,10 @@ namespace Avalonia.FreeDesktop
{
var item = GetMenu(id).item;
if (item is NativeMenuItem menuItem)
if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
{
if (menuItem?.Enabled == true)
menuItem.RaiseClick();
if (menuItem?.IsEnabled == true)
bridge?.RaiseClicked();
}
}
}

14
src/Avalonia.Input/FocusManager.cs

@ -53,11 +53,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Focus(
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
if (control != null)
{
@ -67,7 +67,7 @@ namespace Avalonia.Input
if (scope != null)
{
Scope = scope;
SetFocusedElement(scope, control, method, modifiers);
SetFocusedElement(scope, control, method, keyModifiers);
}
}
else if (Current != null)
@ -95,7 +95,7 @@ namespace Avalonia.Input
/// <param name="scope">The focus scope.</param>
/// <param name="element">The element to focus. May be null.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <remarks>
/// If the specified scope is the current <see cref="Scope"/> then the keyboard focus
/// will change.
@ -104,7 +104,7 @@ namespace Avalonia.Input
IFocusScope scope,
IInputElement element,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires<ArgumentNullException>(scope != null);
@ -123,7 +123,7 @@ namespace Avalonia.Input
if (Scope == scope)
{
KeyboardDevice.Instance?.SetFocusedElement(element, method, modifiers);
KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers);
}
}
@ -195,7 +195,7 @@ namespace Avalonia.Input
{
if (element is IInputElement inputElement && CanFocus(inputElement))
{
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers);
break;
}

6
src/Avalonia.Input/IFocusManager.cs

@ -20,11 +20,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Focus(
IInputElement control,
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None);
KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary>
/// Notifies the focus manager of a change in focus scope.

2
src/Avalonia.Input/IKeyboardDevice.cs

@ -63,6 +63,6 @@ namespace Avalonia.Input
void SetFocusedElement(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers);
KeyModifiers modifiers);
}
}

6
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -19,10 +19,10 @@ namespace Avalonia.Input
/// </summary>
/// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Move(
IInputElement element,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None);
KeyModifiers keyModifiers = KeyModifiers.None);
}
}
}

4
src/Avalonia.Input/KeyboardDevice.cs

@ -35,7 +35,7 @@ namespace Avalonia.Input
public void SetFocusedElement(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers)
KeyModifiers keyModifiers)
{
if (element != FocusedElement)
{
@ -53,7 +53,7 @@ namespace Avalonia.Input
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method,
InputModifiers = modifiers,
KeyModifiers = keyModifiers,
});
}
}

8
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -91,11 +91,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move(
IInputElement element,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires<ArgumentNullException>(element != null);
@ -106,7 +106,7 @@ namespace Avalonia.Input
var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance.Focus(next, method, modifiers);
FocusManager.Instance.Focus(next, method, keyModifiers);
}
}
@ -123,7 +123,7 @@ namespace Avalonia.Input
{
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.Modifiers);
Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
}

2
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the type of the event.
/// </summary>
public RawPointerEventType Type { get; private set; }
public RawPointerEventType Type { get; set; }
/// <summary>
/// Gets the input modifiers.

104
src/Avalonia.Layout/AttachedLayout.cs

@ -46,7 +46,23 @@ namespace Avalonia.Layout
/// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
/// this method in a derived class.
/// </remarks>
public abstract void InitializeForContext(LayoutContext context);
public void InitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.InitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Removes any state the layout previously stored on the ILayoutable container.
@ -55,7 +71,23 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
public abstract void UninitializeForContext(LayoutContext context);
public void UninitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.UninitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Suggests a DesiredSize for a container element. A container element that supports
@ -73,7 +105,23 @@ namespace Avalonia.Layout
/// if scrolling or other resize behavior is possible in that particular container.
/// </param>
/// <returns></returns>
public abstract Size Measure(LayoutContext context, Size availableSize);
public Size Measure(LayoutContext context, Size availableSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Positions child elements and determines a size for a container UIElement. Container
@ -88,7 +136,23 @@ namespace Avalonia.Layout
/// The final size that the container computes for the child in layout.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
public abstract Size Arrange(LayoutContext context, Size finalSize);
public Size Arrange(LayoutContext context, Size finalSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously.
/// </summary>
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
{
if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext;
}
else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext.GetVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
{
if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext;
}
else if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext.GetNonVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
}
}

45
src/Avalonia.Layout/LayoutContextAdapter.cs

@ -0,0 +1,45 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Layout
{
internal class LayoutContextAdapter : VirtualizingLayoutContext
{
private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
{
_nonVirtualizingContext = nonVirtualizingContext;
}
protected override object LayoutStateCore
{
get => _nonVirtualizingContext.LayoutState;
set => _nonVirtualizingContext.LayoutState = value;
}
protected override Point LayoutOriginCore
{
get => default;
set
{
if (value != default)
{
throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
}
}
}
protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
_nonVirtualizingContext.Children[index];
protected override void RecycleElementCore(ILayoutable element) { }
}
}

50
src/Avalonia.Layout/LayoutHelper.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Layout
@ -19,16 +20,11 @@ namespace Avalonia.Layout
/// <returns>The control's size.</returns>
public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
{
var controlWidth = control.Width;
var controlHeight = control.Height;
double width = (controlWidth > 0) ? controlWidth : constraints.Width;
double height = (controlHeight > 0) ? controlHeight : constraints.Height;
width = Math.Min(width, control.MaxWidth);
width = Math.Max(width, control.MinWidth);
height = Math.Min(height, control.MaxHeight);
height = Math.Max(height, control.MinHeight);
return new Size(width, height);
var minmax = new MinMax(control);
return new Size(
MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
}
public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding,
@ -85,5 +81,39 @@ namespace Avalonia.Layout
InnerInvalidateMeasure(control);
}
/// <summary>
/// Calculates the min and max height for a control. Ported from WPF.
/// </summary>
private readonly struct MinMax
{
public MinMax(ILayoutable e)
{
MaxHeight = e.MaxHeight;
MinHeight = e.MinHeight;
double l = e.Height;
double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
height = (double.IsNaN(l) ? 0 : l);
MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
MaxWidth = e.MaxWidth;
MinWidth = e.MinWidth;
l = e.Width;
double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
width = (double.IsNaN(l) ? 0 : l);
MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
}
public double MinWidth { get; }
public double MaxWidth { get; }
public double MinHeight { get; }
public double MaxHeight { get; }
}
}
}

36
src/Avalonia.Layout/NonVirtualizingLayout.cs

@ -17,30 +17,6 @@ namespace Avalonia.Layout
/// </remarks>
public abstract class NonVirtualizingLayout : AttachedLayout
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
}
/// <summary>
/// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container.
@ -49,7 +25,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void InitializeForContextCore(LayoutContext context)
protected internal virtual void InitializeForContextCore(LayoutContext context)
{
}
@ -61,7 +37,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void UninitializeForContextCore(LayoutContext context)
protected internal virtual void UninitializeForContextCore(LayoutContext context)
{
}
@ -83,7 +59,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
protected internal virtual Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize) => finalSize;
}
}

17
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@ -3,6 +3,8 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
namespace Avalonia.Layout
{
/// <summary>
@ -10,5 +12,20 @@ namespace Avalonia.Layout
/// </summary>
public abstract class NonVirtualizingLayoutContext : LayoutContext
{
private VirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the collection of child controls from the container that provides the context.
/// </summary>
public IReadOnlyList<ILayoutable> Children => ChildrenCore;
/// <summary>
/// Implements the behavior for getting the return value of <see cref="Children"/> in a
/// derived or custom <see cref="NonVirtualizingLayoutContext"/>.
/// </summary>
protected abstract IReadOnlyList<ILayoutable> ChildrenCore { get; }
internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
}
}

160
src/Avalonia.Layout/NonVirtualizingStackLayout.cs

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Layout
{
public class NonVirtualizingStackLayout : NonVirtualizingLayout
{
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
/// direction of the StackLayout's Orientation.
/// </summary>
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
protected internal override Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize)
{
var extentU = 0.0;
var extentV = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var constraint = isVertical ?
availableSize.WithHeight(double.PositiveInfinity) :
availableSize.WithWidth(double.PositiveInfinity);
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
element.Measure(constraint);
if (isVertical)
{
extentU += element.DesiredSize.Height;
extentV = Math.Max(extentV, element.DesiredSize.Width);
}
else
{
extentU += element.DesiredSize.Width;
extentV = Math.Max(extentV, element.DesiredSize.Height);
}
if (i < childCount - 1)
{
extentU += spacing;
}
}
return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
}
protected internal override Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize)
{
var u = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var bounds = new Rect();
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
bounds = isVertical ?
LayoutVertical(element, u, finalSize) :
LayoutHorizontal(element, u, finalSize);
element.Arrange(bounds);
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
}
return new Size(bounds.Right, bounds.Bottom);
}
private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)
{
var x = 0.0;
var width = element.DesiredSize.Width;
switch (element.HorizontalAlignment)
{
case HorizontalAlignment.Center:
x += (constraint.Width - element.DesiredSize.Width) / 2;
break;
case HorizontalAlignment.Right:
x += constraint.Width - element.DesiredSize.Width;
break;
case HorizontalAlignment.Stretch:
width = constraint.Width;
break;
}
return new Rect(x, y, width, element.DesiredSize.Height);
}
private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint)
{
var y = 0.0;
var height = element.DesiredSize.Height;
switch (element.VerticalAlignment)
{
case VerticalAlignment.Center:
y += (constraint.Height - element.DesiredSize.Height) / 2;
break;
case VerticalAlignment.Bottom:
y += constraint.Height - element.DesiredSize.Height;
break;
case VerticalAlignment.Stretch:
height = constraint.Height;
break;
}
return new Rect(x, y, element.DesiredSize.Width, height);
}
}
}

8
src/Avalonia.Layout/StackLayout.cs

@ -234,7 +234,7 @@ namespace Avalonia.Layout
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
@ -275,7 +275,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,

8
src/Avalonia.Layout/UniformGridLayout.cs

@ -392,7 +392,7 @@ namespace Avalonia.Layout
{
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var gridState = state as UniformGridLayoutState;
@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
// Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
@ -442,7 +442,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,

42
src/Avalonia.Layout/VirtualLayoutContextAdapter.cs

@ -0,0 +1,42 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Layout
{
public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
{
private readonly VirtualizingLayoutContext _virtualizingContext;
private ChildrenCollection _children;
public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
{
_virtualizingContext = virtualizingContext;
}
protected override object LayoutStateCore
{
get => _virtualizingContext.LayoutState;
set => _virtualizingContext.LayoutState = value;
}
protected override IReadOnlyList<ILayoutable> ChildrenCore =>
_children ?? (_children = new ChildrenCollection(_virtualizingContext));
private class ChildrenCollection : IReadOnlyList<ILayoutable>
{
private readonly VirtualizingLayoutContext _context;
public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
public ILayoutable this[int index] => _context.GetOrCreateElementAt(index);
public int Count => _context.ItemCount;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<ILayoutable> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
}
}
}

36
src/Avalonia.Layout/VirtualizingLayout.cs

@ -19,30 +19,6 @@ namespace Avalonia.Layout
/// </remarks>
public abstract class VirtualizingLayout : AttachedLayout
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
}
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{
}
@ -82,7 +58,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{
}
@ -104,7 +80,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
protected internal virtual Size ArrangeOverride(
VirtualizingLayoutContext context,
Size finalSize) => finalSize;
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)

5
src/Avalonia.Layout/VirtualizingLayoutContext.cs

@ -43,6 +43,8 @@ namespace Avalonia.Layout
/// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext
{
private NonVirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the number of items in the data.
/// </summary>
@ -186,5 +188,8 @@ namespace Avalonia.Layout
/// </summary>
/// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(ILayoutable element);
internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
}
}

415
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -1,185 +1,21 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Dialogs;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using Avalonia.Dialogs;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Native
{
enum OsxUnicodeSpecialKey
{
NSUpArrowFunctionKey = 0xF700,
NSDownArrowFunctionKey = 0xF701,
NSLeftArrowFunctionKey = 0xF702,
NSRightArrowFunctionKey = 0xF703,
NSF1FunctionKey = 0xF704,
NSF2FunctionKey = 0xF705,
NSF3FunctionKey = 0xF706,
NSF4FunctionKey = 0xF707,
NSF5FunctionKey = 0xF708,
NSF6FunctionKey = 0xF709,
NSF7FunctionKey = 0xF70A,
NSF8FunctionKey = 0xF70B,
NSF9FunctionKey = 0xF70C,
NSF10FunctionKey = 0xF70D,
NSF11FunctionKey = 0xF70E,
NSF12FunctionKey = 0xF70F,
NSF13FunctionKey = 0xF710,
NSF14FunctionKey = 0xF711,
NSF15FunctionKey = 0xF712,
NSF16FunctionKey = 0xF713,
NSF17FunctionKey = 0xF714,
NSF18FunctionKey = 0xF715,
NSF19FunctionKey = 0xF716,
NSF20FunctionKey = 0xF717,
NSF21FunctionKey = 0xF718,
NSF22FunctionKey = 0xF719,
NSF23FunctionKey = 0xF71A,
NSF24FunctionKey = 0xF71B,
NSF25FunctionKey = 0xF71C,
NSF26FunctionKey = 0xF71D,
NSF27FunctionKey = 0xF71E,
NSF28FunctionKey = 0xF71F,
NSF29FunctionKey = 0xF720,
NSF30FunctionKey = 0xF721,
NSF31FunctionKey = 0xF722,
NSF32FunctionKey = 0xF723,
NSF33FunctionKey = 0xF724,
NSF34FunctionKey = 0xF725,
NSF35FunctionKey = 0xF726,
NSInsertFunctionKey = 0xF727,
NSDeleteFunctionKey = 0xF728,
NSHomeFunctionKey = 0xF729,
NSBeginFunctionKey = 0xF72A,
NSEndFunctionKey = 0xF72B,
NSPageUpFunctionKey = 0xF72C,
NSPageDownFunctionKey = 0xF72D,
NSPrintScreenFunctionKey = 0xF72E,
NSScrollLockFunctionKey = 0xF72F,
NSPauseFunctionKey = 0xF730,
NSSysReqFunctionKey = 0xF731,
NSBreakFunctionKey = 0xF732,
NSResetFunctionKey = 0xF733,
NSStopFunctionKey = 0xF734,
NSMenuFunctionKey = 0xF735,
NSUserFunctionKey = 0xF736,
NSSystemFunctionKey = 0xF737,
NSPrintFunctionKey = 0xF738,
NSClearLineFunctionKey = 0xF739,
NSClearDisplayFunctionKey = 0xF73A,
NSInsertLineFunctionKey = 0xF73B,
NSDeleteLineFunctionKey = 0xF73C,
NSInsertCharFunctionKey = 0xF73D,
NSDeleteCharFunctionKey = 0xF73E,
NSPrevFunctionKey = 0xF73F,
NSNextFunctionKey = 0xF740,
NSSelectFunctionKey = 0xF741,
NSExecuteFunctionKey = 0xF742,
NSUndoFunctionKey = 0xF743,
NSRedoFunctionKey = 0xF744,
NSFindFunctionKey = 0xF745,
NSHelpFunctionKey = 0xF746,
NSModeSwitchFunctionKey = 0xF747
}
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>();
private static Dictionary<Key, OsxUnicodeSpecialKey> osxKeys = new Dictionary<Key, OsxUnicodeSpecialKey>
{
{Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey },
{Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey },
{Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey },
{Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey },
{ Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey },
{ Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey },
{ Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey },
{ Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey },
{ Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey },
{ Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey },
{ Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey },
{ Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey },
{ Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey },
{ Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey },
{ Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey },
{ Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey },
{ Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey },
{ Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey },
{ Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey },
{ Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey },
{ Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey },
{ Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey },
{ Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey },
{ Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey },
{ Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey },
{ Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey },
{ Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey },
{ Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey },
{ Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey },
{ Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey },
{ Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey },
//{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey },
{ Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey },
{ Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey },
{ Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey },
{ Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey },
{ Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey },
//{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey },
//{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey },
//{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey },
//{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey },
//{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey },
//{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey },
//{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey },
{ Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey },
//{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey },
//{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey },
};
private NativeMenu _menu;
private IAvnMenu _nativeMenu;
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
{
@ -193,7 +29,6 @@ namespace Avalonia.Native
{
_factory = factory;
_menu = NativeMenu.GetMenu(Application.Current);
DoLayoutReset();
}
@ -203,17 +38,19 @@ namespace Avalonia.Native
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;
_menu = menu == null ? new NativeMenu() : menu;
DoLayoutReset();
}
internal void UpdateIfNeeded()
{
if (_resetQueued)
{
DoLayoutReset();
}
}
private static NativeMenu CreateDefaultAppMenu()
{
var result = new NativeMenu();
@ -237,50 +74,34 @@ namespace Avalonia.Native
return result;
}
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)
if (_nativeWindow is null)
{
_menu = NativeMenu.GetMenu(Application.Current);
var appMenu = NativeMenu.GetMenu(Application.Current);
if(_menu != null)
{
SetMenu(_menu);
}
else
if (appMenu == null)
{
SetMenu(CreateDefaultAppMenu());
appMenu = CreateDefaultAppMenu();
NativeMenu.SetMenu(Application.Current, appMenu);
}
SetMenu(appMenu);
}
else
{
SetMenu(_nativeWindow, _menu?.Items);
if (_menu != null)
{
SetMenu(_nativeWindow, _menu);
}
}
_exported = true;
}
private void QueueReset()
internal void QueueReset()
{
if (_resetQueued)
return;
@ -288,188 +109,64 @@ namespace Avalonia.Native
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
}
private IAvnAppMenu CreateSubmenu(ICollection<NativeMenuItemBase> children)
private void SetMenu(NativeMenu menu)
{
var menu = _factory.CreateMenu();
SetChildren(menu, children);
var menuItem = menu.Parent;
return menu;
}
var appMenuHolder = menuItem?.Parent;
private void AddMenuItem(NativeMenuItem item)
{
if (item.Menu?.Items != null)
if (menu.Parent is null)
{
((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged;
menuItem = new NativeMenuItem();
}
}
private static string ConvertOSXSpecialKeyCodes(Key key)
{
if (osxKeys.ContainsKey(key))
{
return ((char)osxKeys[key]).ToString();
}
else
if (appMenuHolder is null)
{
return key.ToString().ToLower();
}
}
appMenuHolder = new NativeMenu();
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(ConvertOSXSpecialKeyCodes(item.Gesture.Key)))
{
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());
}
appMenuHolder.Add(menuItem);
}
}
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());
}
}
}
menuItem.Menu = menu;
private void SetMenu(NativeMenu menu)
{
var appMenu = _factory.ObtainAppMenu();
var setMenu = false;
if (appMenu is null)
if (_nativeMenu is null)
{
appMenu = _factory.CreateMenu();
}
_nativeMenu = IAvnMenu.Create(_factory);
var menuItem = menu.Parent;
_nativeMenu.Initialise(this, appMenuHolder, "");
if(menu.Parent is null)
{
menuItem = new NativeMenuItem();
setMenu = true;
}
menuItem.Menu = menu;
appMenu.Clear();
AddItemsToMenu(appMenu, new List<NativeMenuItemBase> { menuItem });
_nativeMenu.Update(_factory, appMenuHolder);
_factory.SetAppMenu(appMenu);
if (setMenu)
{
_factory.SetAppMenu(_nativeMenu);
}
}
private void SetMenu(IAvnWindow avnWindow, ICollection<NativeMenuItemBase> menuItems)
private void SetMenu(IAvnWindow avnWindow, NativeMenu menu)
{
if (menuItems is null)
var setMenu = false;
if (_nativeMenu is null)
{
menuItems = new List<NativeMenuItemBase>();
}
_nativeMenu = IAvnMenu.Create(_factory);
var appMenu = avnWindow.ObtainMainMenu();
_nativeMenu.Initialise(this, menu, "");
if (appMenu is null)
{
appMenu = _factory.CreateMenu();
setMenu = true;
}
appMenu.Clear();
AddItemsToMenu(appMenu, menuItems);
_nativeMenu.Update(_factory, menu);
avnWindow.SetMainMenu(appMenu);
if(setMenu)
{
avnWindow.SetMainMenu(_nativeMenu);
}
}
}
}

176
src/Avalonia.Native/IAvnMenu.cs

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Platform.Interop;
namespace Avalonia.Native.Interop
{
class MenuEvents : CallbackBase, IAvnMenuEvents
{
private IAvnMenu _parent;
public void Initialise(IAvnMenu parent)
{
_parent = parent;
}
public void NeedsUpdate()
{
_parent?.RaiseNeedsUpdate();
}
}
public partial class IAvnMenu
{
private MenuEvents _events;
private AvaloniaNativeMenuExporter _exporter;
private List<IAvnMenuItem> _menuItems = new List<IAvnMenuItem>();
private Dictionary<NativeMenuItemBase, IAvnMenuItem> _menuItemLookup = new Dictionary<NativeMenuItemBase, IAvnMenuItem>();
private CompositeDisposable _propertyDisposables = new CompositeDisposable();
internal void RaiseNeedsUpdate()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate();
_exporter.UpdateIfNeeded();
}
internal NativeMenu ManagedMenu { get; private set; }
public static IAvnMenu Create(IAvaloniaNativeFactory factory)
{
var events = new MenuEvents();
var menu = factory.CreateMenu(events);
events.Initialise(menu);
menu._events = events;
return menu;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_events.Dispose();
}
}
private void RemoveAndDispose(IAvnMenuItem item)
{
_menuItemLookup.Remove(item.ManagedMenuItem);
_menuItems.Remove(item);
RemoveItem(item);
item.Deinitialise();
item.Dispose();
}
private void MoveExistingTo(int index, IAvnMenuItem item)
{
_menuItems.Remove(item);
_menuItems.Insert(index, item);
RemoveItem(item);
InsertItem(index, item);
}
private IAvnMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item)
{
var result = CreateNew(factory, item);
result.Initialise(item);
_menuItemLookup.Add(result.ManagedMenuItem, result);
_menuItems.Insert(index, result);
InsertItem(index, result);
return result;
}
private IAvnMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
{
var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem();
nativeItem.ManagedMenuItem = item;
return nativeItem;
}
internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title)
{
_exporter = exporter;
ManagedMenu = managedMenu;
((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged;
if (!string.IsNullOrWhiteSpace(title))
{
using (var buffer = new Utf8Buffer(title))
{
Title = buffer.DangerousGetHandle();
}
}
}
internal void Deinitialise()
{
((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged;
foreach (var item in _menuItems)
{
item.Deinitialise();
item.Dispose();
}
}
internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu)
{
if (menu != ManagedMenu)
{
throw new ArgumentException("The menu being updated does not match.", nameof(menu));
}
for (int i = 0; i < menu.Items.Count; i++)
{
IAvnMenuItem nativeItem;
if (i >= _menuItems.Count)
{
nativeItem = CreateNewAt(factory, i, menu.Items[i]);
}
else if (menu.Items[i] == _menuItems[i].ManagedMenuItem)
{
nativeItem = _menuItems[i];
}
else if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem))
{
MoveExistingTo(i, nativeItem);
}
else
{
nativeItem = CreateNewAt(factory, i, menu.Items[i]);
}
if (menu.Items[i] is NativeMenuItem nmi)
{
nativeItem.Update(_exporter, factory, nmi);
}
}
while (_menuItems.Count > menu.Items.Count)
{
RemoveAndDispose(_menuItems[_menuItems.Count - 1]);
}
}
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
_exporter.QueueReset();
}
}
}

175
src/Avalonia.Native/IAvnMenuItem.cs

@ -0,0 +1,175 @@
using System;
using System.IO;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Interop;
namespace Avalonia.Native.Interop
{
public partial class IAvnMenuItem
{
private IAvnMenu _subMenu;
private CompositeDisposable _propertyDisposables = new CompositeDisposable();
private IDisposable _currentActionDisposable;
public NativeMenuItemBase ManagedMenuItem { get; set; }
private void UpdateTitle(string title)
{
using (var buffer = new Utf8Buffer(string.IsNullOrWhiteSpace(title) ? "" : title))
{
Title = buffer.DangerousGetHandle();
}
}
private void UpdateIsChecked(bool isChecked)
{
IsChecked = isChecked;
}
private void UpdateToggleType(NativeMenuItemToggleType toggleType)
{
ToggleType = (AvnMenuItemToggleType)toggleType;
}
private unsafe void UpdateIcon (IBitmap icon)
{
if(icon is null)
{
SetIcon(IntPtr.Zero, 0);
}
else
{
using(var ms = new MemoryStream())
{
icon.Save(ms);
var imageData = ms.ToArray();
fixed(void* ptr = imageData)
{
SetIcon(new IntPtr(ptr), imageData.Length);
}
}
}
}
private void UpdateGesture(Input.KeyGesture gesture)
{
// todo ensure backend can cope with setting null gesture.
using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key)))
{
var modifiers = gesture == null ? AvnInputModifiers.AvnInputModifiersNone : (AvnInputModifiers)gesture.KeyModifiers;
SetGesture(buffer.DangerousGetHandle(), modifiers);
}
}
private void UpdateAction(NativeMenuItem item)
{
_currentActionDisposable?.Dispose();
var action = new PredicateCallback(() =>
{
if (item.Command != null || item.HasClickHandlers)
{
return item.IsEnabled;
}
return false;
});
var callback = new MenuActionCallback(() => { (item as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); });
_currentActionDisposable = Disposable.Create(() =>
{
action.Dispose();
callback.Dispose();
});
SetAction(action, callback);
}
internal void Initialise(NativeMenuItemBase nativeMenuItem)
{
ManagedMenuItem = nativeMenuItem;
if (ManagedMenuItem is NativeMenuItem item)
{
UpdateTitle(item.Header);
UpdateGesture(item.Gesture);
UpdateAction(ManagedMenuItem as NativeMenuItem);
UpdateToggleType(item.ToggleType);
UpdateIcon(item.Icon);
UpdateIsChecked(item.IsChecked);
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty)
.Subscribe(x => UpdateTitle(x)));
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty)
.Subscribe(x => UpdateGesture(x)));
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty)
.Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)));
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.ToggleTypeProperty)
.Subscribe(x => UpdateToggleType(x)));
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty)
.Subscribe(x => UpdateIsChecked(x)));
_propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty)
.Subscribe(x => UpdateIcon(x)));
}
}
internal void Deinitialise()
{
if (_subMenu != null)
{
SetSubMenu(null);
_subMenu.Deinitialise();
_subMenu.Dispose();
_subMenu = null;
}
_propertyDisposables?.Dispose();
_currentActionDisposable?.Dispose();
}
internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item)
{
if (item != ManagedMenuItem)
{
throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item));
}
if (item.Menu != null)
{
if (_subMenu == null)
{
_subMenu = IAvnMenu.Create(factory);
_subMenu.Initialise(exporter, item.Menu, item.Header);
SetSubMenu(_subMenu);
}
_subMenu.Update(factory, item.Menu);
}
if (item.Menu == null && _subMenu != null)
{
_subMenu.Deinitialise();
_subMenu.Dispose();
SetSubMenu(null);
}
}
}
}

2
src/Avalonia.Native/Mappings.xml

@ -19,5 +19,7 @@
<map param=".*::.*::ppv" return="true"/>
<map param=".*::.*::ret" return="true"/>
<map param=".*::.*::retOut" attribute="out" return="true"/>
<map method="IAvnAppMenu:.*" visibility="private" />
<map method="IAvnAppMenuItem:.*" visibility="private" />
</mapping>
</config>

20
src/Avalonia.Native/MenuActionCallback.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Native.Interop;
namespace Avalonia.Native
{
public class MenuActionCallback : CallbackBase, IAvnActionCallback
{
private Action _action;
public MenuActionCallback(Action action)
{
_action = action;
}
void IAvnActionCallback.Run()
{
_action?.Invoke();
}
}
}

147
src/Avalonia.Native/OsxUnicodeKeys.cs

@ -0,0 +1,147 @@
using System.Collections.Generic;
using Avalonia.Input;
namespace Avalonia.Native.Interop
{
internal static class OsxUnicodeKeys
{
enum OsxUnicodeSpecialKey
{
NSUpArrowFunctionKey = 0xF700,
NSDownArrowFunctionKey = 0xF701,
NSLeftArrowFunctionKey = 0xF702,
NSRightArrowFunctionKey = 0xF703,
NSF1FunctionKey = 0xF704,
NSF2FunctionKey = 0xF705,
NSF3FunctionKey = 0xF706,
NSF4FunctionKey = 0xF707,
NSF5FunctionKey = 0xF708,
NSF6FunctionKey = 0xF709,
NSF7FunctionKey = 0xF70A,
NSF8FunctionKey = 0xF70B,
NSF9FunctionKey = 0xF70C,
NSF10FunctionKey = 0xF70D,
NSF11FunctionKey = 0xF70E,
NSF12FunctionKey = 0xF70F,
NSF13FunctionKey = 0xF710,
NSF14FunctionKey = 0xF711,
NSF15FunctionKey = 0xF712,
NSF16FunctionKey = 0xF713,
NSF17FunctionKey = 0xF714,
NSF18FunctionKey = 0xF715,
NSF19FunctionKey = 0xF716,
NSF20FunctionKey = 0xF717,
NSF21FunctionKey = 0xF718,
NSF22FunctionKey = 0xF719,
NSF23FunctionKey = 0xF71A,
NSF24FunctionKey = 0xF71B,
NSF25FunctionKey = 0xF71C,
NSF26FunctionKey = 0xF71D,
NSF27FunctionKey = 0xF71E,
NSF28FunctionKey = 0xF71F,
NSF29FunctionKey = 0xF720,
NSF30FunctionKey = 0xF721,
NSF31FunctionKey = 0xF722,
NSF32FunctionKey = 0xF723,
NSF33FunctionKey = 0xF724,
NSF34FunctionKey = 0xF725,
NSF35FunctionKey = 0xF726,
NSInsertFunctionKey = 0xF727,
NSDeleteFunctionKey = 0xF728,
NSHomeFunctionKey = 0xF729,
NSBeginFunctionKey = 0xF72A,
NSEndFunctionKey = 0xF72B,
NSPageUpFunctionKey = 0xF72C,
NSPageDownFunctionKey = 0xF72D,
NSPrintScreenFunctionKey = 0xF72E,
NSScrollLockFunctionKey = 0xF72F,
NSPauseFunctionKey = 0xF730,
NSSysReqFunctionKey = 0xF731,
NSBreakFunctionKey = 0xF732,
NSResetFunctionKey = 0xF733,
NSStopFunctionKey = 0xF734,
NSMenuFunctionKey = 0xF735,
NSUserFunctionKey = 0xF736,
NSSystemFunctionKey = 0xF737,
NSPrintFunctionKey = 0xF738,
NSClearLineFunctionKey = 0xF739,
NSClearDisplayFunctionKey = 0xF73A,
NSInsertLineFunctionKey = 0xF73B,
NSDeleteLineFunctionKey = 0xF73C,
NSInsertCharFunctionKey = 0xF73D,
NSDeleteCharFunctionKey = 0xF73E,
NSPrevFunctionKey = 0xF73F,
NSNextFunctionKey = 0xF740,
NSSelectFunctionKey = 0xF741,
NSExecuteFunctionKey = 0xF742,
NSUndoFunctionKey = 0xF743,
NSRedoFunctionKey = 0xF744,
NSFindFunctionKey = 0xF745,
NSHelpFunctionKey = 0xF746,
NSModeSwitchFunctionKey = 0xF747
}
private static Dictionary<Key, OsxUnicodeSpecialKey> s_osxKeys = new Dictionary<Key, OsxUnicodeSpecialKey>
{
{Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey },
{Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey },
{Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey },
{Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey },
{ Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey },
{ Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey },
{ Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey },
{ Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey },
{ Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey },
{ Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey },
{ Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey },
{ Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey },
{ Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey },
{ Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey },
{ Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey },
{ Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey },
{ Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey },
{ Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey },
{ Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey },
{ Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey },
{ Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey },
{ Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey },
{ Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey },
{ Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey },
{ Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey },
{ Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey },
{ Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey },
{ Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey },
{ Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey },
{ Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey },
{ Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey },
//{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey },
{ Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey },
{ Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey },
{ Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey },
{ Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey },
{ Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey },
//{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey },
//{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey },
//{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey },
//{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey },
//{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey },
//{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey },
//{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey },
{ Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey },
//{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey },
//{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey },
};
public static string ConvertOSXSpecialKeyCodes(Key key)
{
if (s_osxKeys.ContainsKey(key))
{
return ((char)s_osxKeys[key]).ToString();
}
else
{
return key.ToString().ToLower();
}
}
}
}

20
src/Avalonia.Native/PredicateCallback.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Native.Interop;
namespace Avalonia.Native
{
public class PredicateCallback : CallbackBase, IAvnPredicateCallback
{
private Func<bool> _predicate;
public PredicateCallback(Func<bool> predicate)
{
_predicate = predicate;
}
bool IAvnPredicateCallback.Evaluate()
{
return _predicate();
}
}
}

20
src/Avalonia.Native/SystemDialogs.cs

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform;
namespace Avalonia.Native
{
@ -18,13 +17,15 @@ namespace Avalonia.Native
_native = native;
}
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
var events = new SystemDialogEvents();
var nativeParent = GetNativeWindow(parent);
if (dialog is OpenFileDialog ofd)
{
_native.OpenFileDialog((parent as WindowImpl)?.Native,
_native.OpenFileDialog(nativeParent,
events, ofd.AllowMultiple,
ofd.Title ?? "",
ofd.InitialDirectory ?? "",
@ -33,7 +34,7 @@ namespace Avalonia.Native
}
else
{
_native.SaveFileDialog((parent as WindowImpl)?.Native,
_native.SaveFileDialog(nativeParent,
events,
dialog.Title ?? "",
dialog.InitialDirectory ?? "",
@ -44,14 +45,21 @@ namespace Avalonia.Native
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; });
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
var events = new SystemDialogEvents();
_native.SelectFolderDialog((parent as WindowImpl)?.Native, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
var nativeParent = GetNativeWindow(parent);
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); });
}
private IAvnWindow GetNativeWindow(Window window)
{
return (window?.PlatformImpl as WindowImpl)?.Native;
}
}
public class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents

2
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI
public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
return builder.AfterSetup(_ =>
return builder.AfterPlatformServicesSetup(_ =>
{
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));

26
src/Avalonia.Visuals/Media/FontManager.cs

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
}
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
if (fontManagerImpl == null)
throw new InvalidOperationException("No font manager implementation was registered.");
current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily;
}
var key = new FontKey(fontFamily, fontWeight, fontStyle);
var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface))
{
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
foreach (var cachedTypeface in _typefaceCache.Values)
{
// First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
&& cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
{
return cachedTypeface;
}
}
var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
null;
return matchedTypeface;
}
}
}

3
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Utilities;
@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames));
}
Names = familyNames.Split(',').Select(x => x.Trim()).ToArray();
Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0];

16
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
{
public readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontFamily FontFamily;
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
public FontKey(string familyName, FontWeight weight, FontStyle style)
{
FontFamily = fontFamily;
FamilyName = familyName;
Style = style;
Weight = weight;
}
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode()
{
var hash = FontFamily.GetHashCode();
var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other)
{
return FontFamily == other.FontFamily &&
return FamilyName == other.FamilyName &&
Style == other.Style &&
Weight == other.Weight;
}

2
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback
currentTypeface =
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -2,7 +2,7 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
internal ref struct CodepointEnumerator
public ref struct CodepointEnumerator
{
private ReadOnlySlice<char> _text;

44
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs

@ -1,44 +0,0 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
public enum UnicodeGeneralCategory : byte
{
Other, //C# Cc | Cf | Cn | Co | Cs
Control, //Cc
Format, //Cf
Unassigned, //Cn
PrivateUse, //Co
Surrogate, //Cs
Letter, //L# Ll | Lm | Lo | Lt | Lu
CasedLetter, //LC# Ll | Lt | Lu
LowercaseLetter, //Ll
ModifierLetter, //Lm
OtherLetter, //Lo
TitlecaseLetter, //Lt
UppercaseLetter, //Lu
Mark, //M
SpacingMark, //Mc
EnclosingMark, //Me
NonspacingMark, //Mn
Number, //N# Nd | Nl | No
DecimalNumber, //Nd
LetterNumber, //Nl
OtherNumber, //No
Punctuation, //P
ConnectorPunctuation, //Pc
DashPunctuation, //Pd
ClosePunctuation, //Pe
FinalPunctuation, //Pf
InitialPunctuation, //Pi
OtherPunctuation, //Po
OpenPunctuation, //Ps
Symbol, //S# Sc | Sk | Sm | So
CurrencySymbol, //Sc
ModifierSymbol, //Sk
MathSymbol, //Sm
OtherSymbol, //So
Separator, //Z# Zl | Zp | Zs
LineSeparator, //Zl
ParagraphSeparator, //Zp
SpaceSeparator, //Zs
}
}

22
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -91,28 +91,40 @@ namespace Avalonia.X11.NativeDialogs
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName))
gtk_file_chooser_set_current_name(dlg, fn);
{
if (action == GtkFileChooserAction.Save)
gtk_file_chooser_set_current_name(dlg, fn);
else
gtk_file_chooser_set_filename(dlg, fn);
}
gtk_window_present(dlg);
return tcs.Task;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(
() => ShowDialog(dialog.Title, parent,
() => ShowDialog(dialog.Title, platformImpl,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters));
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(dialog.Title, parent,
var res = await ShowDialog(dialog.Title, platformImpl,
GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null);
return res?.FirstOrDefault();
});

10
src/Avalonia.X11/X11ImmediateRendererProxy.cs

@ -11,6 +11,7 @@ namespace Avalonia.X11
private readonly IRenderLoop _loop;
private ImmediateRenderer _renderer;
private bool _invalidated;
private bool _running;
private object _lock = new object();
public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop)
@ -22,6 +23,7 @@ namespace Avalonia.X11
public void Dispose()
{
_running = false;
_renderer.Dispose();
}
@ -78,12 +80,14 @@ namespace Avalonia.X11
public void Start()
{
_running = true;
_loop.Add(this);
_renderer.Start();
}
public void Stop()
{
_running = false;
_loop.Remove(this);
_renderer.Stop();
}
@ -100,7 +104,11 @@ namespace Avalonia.X11
{
lock (_lock)
_invalidated = false;
Dispatcher.UIThread.Post(() => Paint(new Rect(0, 0, 100000, 100000)));
Dispatcher.UIThread.Post(() =>
{
if (_running)
Paint(new Rect(0, 0, 100000, 100000));
});
}
}
}

24
src/Avalonia.X11/X11Window.cs

@ -442,7 +442,7 @@ namespace Avalonia.X11
updatedSizeViaScaling = UpdateScaling();
}
if (changedSize && !updatedSizeViaScaling)
if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
@ -649,7 +649,27 @@ namespace Avalonia.X11
ScheduleInput(args);
}
public void ScheduleInput(RawInputEventArgs args)
public void ScheduleXI2Input(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pargs)
{
if ((pargs.Type == RawPointerEventType.TouchBegin
|| pargs.Type == RawPointerEventType.TouchUpdate
|| pargs.Type == RawPointerEventType.LeftButtonDown
|| pargs.Type == RawPointerEventType.RightButtonDown
|| pargs.Type == RawPointerEventType.MiddleButtonDown
|| pargs.Type == RawPointerEventType.NonClientLeftButtonDown)
&& ActivateTransientChildIfNeeded())
return;
if (pargs.Type == RawPointerEventType.TouchEnd
&& ActivateTransientChildIfNeeded())
pargs.Type = RawPointerEventType.TouchCancel;
}
ScheduleInput(args);
}
private void ScheduleInput(RawInputEventArgs args)
{
if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling;

10
src/Avalonia.X11/XI2Manager.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
@ -230,10 +230,10 @@ namespace Avalonia.X11
}
if (scrollDelta != default)
client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev))
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null
};
if (type.HasValue)
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
}
@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client
{
IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args);
void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; }
}

17
src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs

@ -4,7 +4,8 @@ using System.Globalization;
namespace Avalonia.Markup.Xaml.Converters
{
using System.ComponentModel;
using System.ComponentModel;
using Avalonia.Utilities;
public class PointsListTypeConverter : TypeConverter
{
@ -15,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string strValue = (string)value;
string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var result = new List<Point>(pointStrs.Length);
foreach (var pointStr in pointStrs)
var points = new List<Point>();
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList."))
{
result.Add(Point.Parse(pointStr));
while (tokenizer.TryReadDouble(out double x))
{
points.Add(new Point(x, tokenizer.ReadDouble()));
}
}
return result;
return points;
}
}
}

1
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -100,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
=> AddType(typeSystem.GetType(type), typeSystem.GetType(conv));
Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));

40
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -104,6 +104,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
if (results != null && result != null)
{
results.Add(result);
}
return results ?? result;
}
@ -158,9 +163,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func<IXamlIlMethod, bool> method)
{
var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors");
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 &&
m.Parameters[0].FullName == "Avalonia.Styling.Selector"
&& method(m));
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m));
codeGen.EmitCall(found);
}
}
@ -308,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
_selectors.Add(node);
}
//TODO: actually find the type
public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.TargetType;
public override IXamlIlType TargetType
{
get
{
IXamlIlType result = null;
foreach (var selector in _selectors)
{
if (selector.TargetType == null)
{
return null;
}
else if (result == null)
{
result = selector.TargetType;
}
else
{
while (!result.IsAssignableFrom(selector.TargetType))
{
result = result.BaseType;
}
}
}
return result;
}
}
protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
{
if (_selectors.Count == 0)

67
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -30,6 +30,10 @@ namespace Avalonia.Skia
private Matrix _currentTransform;
private GRContext _grContext;
private bool _disposed;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
/// <summary>
/// Context create info.
/// </summary>
@ -153,7 +157,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
}
@ -165,8 +169,8 @@ namespace Avalonia.Skia
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper))
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var paint = CreatePaint(brush, rect.Size))
using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{
if (isRounded)
{
@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
using (var paint = CreatePaint(pen, rect.Size))
using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{
if (isRounded)
{
@ -222,7 +226,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(foreground, text.Bounds.Size))
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -232,14 +236,14 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint);
ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint);
(float)baselineOrigin.Y, paintWrapper.Paint);
}
}
@ -323,7 +327,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(mask, bounds.Size));
_maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
}
/// <inheritdoc />
@ -364,6 +368,15 @@ namespace Avalonia.Skia
}
}
internal void ConfigureTextRendering(PaintWrapper wrapper)
{
var paint = wrapper.Paint;
paint.IsEmbeddedBitmapText = true;
paint.SubpixelText = true;
paint.LcdRenderText = _canTextUseLcdRendering;
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>
@ -514,17 +527,16 @@ namespace Avalonia.Skia
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
{
var paint = new SKPaint
{
IsAntialias = true
};
var paintWrapper = new PaintWrapper(paint, disposePaint);
var paintWrapper = new PaintWrapper(paint);
paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity;
@ -572,10 +584,12 @@ namespace Avalonia.Skia
/// <summary>
/// Creates paint wrapper for given pen.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns>
private PaintWrapper CreatePaint(IPen pen, Size targetSize)
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default;
}
var rv = CreatePaint(pen.Brush, targetSize);
var paint = rv.Paint;
var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;
@ -668,7 +681,7 @@ namespace Avalonia.Skia
/// <summary>
/// Skia cached paint state.
/// </summary>
private struct PaintState : IDisposable
private readonly struct PaintState : IDisposable
{
private readonly SKColor _color;
private readonly SKShader _shader;
@ -696,14 +709,16 @@ namespace Avalonia.Skia
{
//We are saving memory allocations there
public readonly SKPaint Paint;
private readonly bool _disposePaint;
private IDisposable _disposable1;
private IDisposable _disposable2;
private IDisposable _disposable3;
public PaintWrapper(SKPaint paint)
public PaintWrapper(SKPaint paint, bool disposePaint)
{
Paint = paint;
_disposePaint = disposePaint;
_disposable1 = null;
_disposable2 = null;
@ -751,7 +766,15 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void Dispose()
{
Paint?.Dispose();
if (_disposePaint)
{
Paint?.Dispose();
}
else
{
Paint?.Reset();
}
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();

46
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -32,6 +32,27 @@ namespace Avalonia.Skia
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
SKFontStyle skFontStyle;
switch (fontWeight)
{
case FontWeight.Normal when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Normal;
break;
case FontWeight.Normal when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.Italic;
break;
case FontWeight.Bold when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Bold;
break;
case FontWeight.Bold when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.BoldItalic;
break;
default:
skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
break;
}
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
if (fontFamily != null)
if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
{
foreach (var familyName in fontFamily.FamilyNames)
var familyNames = fontFamily.FamilyNames;
for (var i = 1; i < familyNames.Count; i++)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
var skTypeface =
_skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
var skTypeface = SKTypeface.Default;
SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null)
{
@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface);
}
if (skTypeface == null)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
return new GlyphTypefaceImpl(skTypeface);
}
}

23
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -140,25 +140,17 @@ namespace Avalonia.Skia
public Rect HitTestTextPosition(int index)
{
if (string.IsNullOrEmpty(Text))
{
var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
return new Rect(alignmentOffset, 0, 0, _lineHeight);
}
var rects = GetRects();
if (index < 0 || index >= rects.Count)
if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
}
if (rects.Count == 0)
{
return new Rect(0, 0, 1, _lineHeight);
}
if (index == rects.Count)
{
var lr = rects[rects.Count - 1];
return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
}
return rects[index];
}
@ -274,7 +266,8 @@ namespace Avalonia.Skia
if (fb != null)
{
//TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(fb, new Size());
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
new Size());
}
else
{

9
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -7,17 +7,11 @@ namespace Avalonia.Skia
/// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
public GlyphRunImpl(SKTextBlob textBlob)
{
Paint = paint;
TextBlob = textBlob;
}
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary>
/// Gets the text blob to draw.
/// </summary>
@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose()
{
TextBlob.Dispose();
Paint.Dispose();
}
}
}

116
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -149,6 +149,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
private static readonly SKPaint s_paint = new SKPaint
{
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
@ -158,92 +168,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface;
var paint = new SKPaint
{
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
s_paint.Typeface = typeface;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
SKTextBlob textBlob;
if (glyphRun.GlyphOffsets.IsEmpty)
{
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
width = 0;
textBlob = textBlobBuilder.Build();
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
if (glyphRun.GlyphOffsets.IsEmpty)
{
if (glyphTypeface.IsFixedPitch)
{
s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
textBlob = s_textBlobBuilder.Build();
textBlob = textBlobBuilder.Build();
}
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
width += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = s_textBlobBuilder.Build();
}
}
else
{
var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
textBlob = textBlobBuilder.Build();
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
}
return new GlyphRunImpl(paint, textBlob);
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = s_textBlobBuilder.Build();
}
return new GlyphRunImpl(textBlob);
}
}
}

4
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -19,7 +19,7 @@ namespace Avalonia.Skia
public SKTypeface Get(Typeface typeface)
{
var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style);
var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
return GetNearestMatch(_typefaces, key);
}
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0)
{
return SKTypeface.Default;
return null;
}
key = keys[0];

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue;
}
var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
typeFaceCollection.AddTypeface(key, typeface);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save