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: inputs:
script: | script: |
brew update brew update
brew install castxml brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'

4
build/SkiaSharp.props

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

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

@ -1,5 +1,6 @@
#include "com.h" #include "com.h"
#include "key.h" #include "key.h"
#include "stddef.h"
#define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id) #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 IAvnGlDisplay;
struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession; struct IAvnGlSurfaceRenderingSession;
struct IAvnAppMenu; struct IAvnMenu;
struct IAvnAppMenuItem; struct IAvnMenuItem;
struct IAvnMenuEvents;
enum SystemDecorations { enum SystemDecorations {
SystemDecorationsNone = 0, SystemDecorationsNone = 0,
@ -175,6 +177,13 @@ enum AvnWindowEdge
WindowEdgeSouthEast WindowEdgeSouthEast
}; };
enum AvnMenuItemToggleType
{
None,
CheckMark,
Radio
};
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{ {
public: public:
@ -188,11 +197,10 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0;
virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0;
virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0;
virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0;
}; };
AVNCOM(IAvnString, 17) : IUnknown AVNCOM(IAvnString, 17) : IUnknown
@ -222,8 +230,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT SetTopMost (bool value) = 0; virtual HRESULT SetTopMost (bool value) = 0;
virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0;
virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
@ -388,10 +395,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown
virtual HRESULT GetScaling(double* ret) = 0; virtual HRESULT GetScaling(double* ret) = 0;
}; };
AVNCOM(IAvnAppMenu, 17) : IUnknown AVNCOM(IAvnMenu, 17) : IUnknown
{ {
virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0;
virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0;
virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT SetTitle (void* utf8String) = 0;
virtual HRESULT Clear () = 0; virtual HRESULT Clear () = 0;
}; };
@ -401,12 +408,23 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown
virtual bool Evaluate() = 0; 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 SetTitle (void* utf8String) = 0;
virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0;
virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 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(); 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"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -58,12 +56,10 @@
</MacroExpansion> </MacroExpansion>
<CommandLineArguments> <CommandLineArguments>
<CommandLineArgument <CommandLineArgument
argument = "bin/Debug/netcoreapp2.0/ControlCatalog.NetCore.dll" argument = "bin/Debug/netcoreapp3.1/ControlCatalog.NetCore.dll"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

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

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

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

@ -2,7 +2,8 @@
@interface AvnAppDelegate : NSObject<NSApplicationDelegate> @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
@end @end
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate @implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification - (void)applicationWillFinishLaunching:(NSNotification *)notification
{ {
@ -14,6 +15,10 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationA
} }
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; [[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 IAvnClipboard* CreateClipboard();
extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay(); extern IAvnGlDisplay* GetGlDisplay();
extern IAvnAppMenu* CreateAppMenu(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnAppMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); extern IAvnMenuItem* CreateAppMenuItemSeperator();
extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnAppMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem (); extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(); 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(); PrivateLSASN asn = ls_get_current_application_asn_func();
// Constant used by WebKit; what exactly it means is unknown. // Constant used by WebKit; what exactly it means is unknown.
const int magic_session_constant = -2; const int magic_session_constant = -2;
OSErr err =
ls_set_application_information_item_func(magic_session_constant, asn, ls_set_application_information_item_func(magic_session_constant, asn,
ls_display_name_key, ls_display_name_key,
process_name, process_name,
NULL /* optional out param */); NULL /* optional out param */);
//LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
} }
class MacOptions : public ComSingleObject<IAvnMacOptions, &IID_IAvnMacOptions> class MacOptions : public ComSingleObject<IAvnMacOptions, &IID_IAvnMacOptions>
@ -228,41 +227,29 @@ public:
return S_OK; 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; return S_OK;
} }
virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override
{ {
*ppv = ::CreateAppMenuItem(); *ppv = ::CreateAppMenuItem();
return S_OK; return S_OK;
} }
virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
{ {
*ppv = ::CreateAppMenuItemSeperator(); *ppv = ::CreateAppMenuItemSeperator();
return S_OK; return S_OK;
} }
virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override
{ {
::SetAppMenu(s_appTitle, appMenu); ::SetAppMenu(s_appTitle, appMenu);
return S_OK; return S_OK;
} }
virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override
{
if(retOut == nullptr)
{
return E_POINTER;
}
*retOut = ::GetAppMenu();
return S_OK;
}
}; };
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

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

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

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

@ -4,6 +4,30 @@
#include "window.h" #include "window.h"
@implementation AvnMenu @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 @end
@implementation AvnMenuItem @implementation AvnMenuItem
@ -46,6 +70,7 @@
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
{ {
_isCheckable = false;
_isSeperator = isSeperator; _isSeperator = isSeperator;
if(isSeperator) if(isSeperator)
@ -65,49 +90,134 @@ NSMenuItem* AvnAppMenuItem::GetNative()
return _native; return _native;
} }
HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu)
{ {
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative(); @autoreleasepool
{
[_native setSubmenu: nsMenu]; if(menu != nullptr)
{
return S_OK; auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
[_native setSubmenu: nsMenu];
}
else
{
[_native setSubmenu: nullptr];
}
return S_OK;
}
} }
HRESULT AvnAppMenuItem::SetTitle (void* utf8String) 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) HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers)
{ {
NSEventModifierFlags flags = 0; @autoreleasepool
{
if (modifiers & Control) NSEventModifierFlags flags = 0;
flags |= NSEventModifierFlagControl;
if (modifiers & Shift) if (modifiers & Control)
flags |= NSEventModifierFlagShift; flags |= NSEventModifierFlagControl;
if (modifiers & Alt) if (modifiers & Shift)
flags |= NSEventModifierFlagOption; flags |= NSEventModifierFlagShift;
if (modifiers & Windows) if (modifiers & Alt)
flags |= NSEventModifierFlagCommand; flags |= NSEventModifierFlagOption;
if (modifiers & Windows)
[_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; flags |= NSEventModifierFlagCommand;
[_native setKeyEquivalentModifierMask:flags];
[_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]];
return S_OK; [_native setKeyEquivalentModifierMask:flags];
return S_OK;
}
} }
HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback)
{ {
_predicate = predicate; @autoreleasepool
_callback = callback; {
return S_OK; _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() 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() AvnMenu* AvnAppMenu::GetNative()
{ {
return _native; return _native;
} }
HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) void AvnAppMenu::RaiseNeedsUpdate()
{ {
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item); if(_baseEvents != nullptr)
if(avnMenuItem != 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); @autoreleasepool
if(avnMenuItem != nullptr)
{ {
[_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) 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() HRESULT AvnAppMenu::Clear()
{ {
[_native removeAllItems]; @autoreleasepool
return S_OK; {
[_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 @autoreleasepool
{ {
id menuBar = [NSMenu new]; return new AvnAppMenu(cb);
return new AvnAppMenu(menuBar);
} }
} }
extern IAvnAppMenuItem* CreateAppMenuItem() extern IAvnMenuItem* CreateAppMenuItem()
{ {
@autoreleasepool @autoreleasepool
{ {
@ -202,7 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem()
} }
} }
extern IAvnAppMenuItem* CreateAppMenuItemSeperator() extern IAvnMenuItem* CreateAppMenuItemSeperator()
{ {
@autoreleasepool @autoreleasepool
{ {
@ -210,10 +372,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
} }
} }
static IAvnAppMenu* s_appMenu = nullptr; static IAvnMenu* s_appMenu = nullptr;
static NSMenuItem* s_appMenuItem = nullptr; static NSMenuItem* s_appMenuItem = nullptr;
extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
{ {
s_appMenu = menu; s_appMenu = menu;
@ -294,7 +456,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
} }
} }
extern IAvnAppMenu* GetAppMenu () extern IAvnMenu* GetAppMenu ()
{ {
return s_appMenu; return s_appMenu;
} }

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

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

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

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

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

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

6
packages/Avalonia/Avalonia.csproj

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

2
packages/Avalonia/Avalonia.props

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

22
samples/ControlCatalog/MainWindow.xaml

@ -14,9 +14,9 @@
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/> <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/> <NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent"> <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
<NativeMenu/> <NativeMenu/>
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
@ -36,6 +36,24 @@
</NativeMenu> </NativeMenu>
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
</NativeMenuItem> </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>
</NativeMenu.Menu> </NativeMenu.Menu>

1
samples/ControlCatalog/MainWindow.xaml.cs

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

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

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

15
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels
{ {
private IManagedNotificationManager _notificationManager; private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true;
public MainWindowViewModel(IManagedNotificationManager notificationManager) public MainWindowViewModel(IManagedNotificationManager notificationManager)
{ {
_notificationManager = notificationManager; _notificationManager = notificationManager;
@ -42,6 +44,11 @@ namespace ControlCatalog.ViewModels
{ {
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
}); });
ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
{
IsMenuItemChecked = !IsMenuItemChecked;
});
} }
public IManagedNotificationManager NotificationManager public IManagedNotificationManager NotificationManager
@ -50,6 +57,12 @@ namespace ControlCatalog.ViewModels
set { this.RaiseAndSetIfChanged(ref _notificationManager, value); } 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> ShowCustomManagedNotificationCommand { get; }
public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; } public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; }
@ -59,5 +72,7 @@ namespace ControlCatalog.ViewModels
public ReactiveCommand<Unit, Unit> AboutCommand { get; } public ReactiveCommand<Unit, Unit> AboutCommand { get; }
public ReactiveCommand<Unit, Unit> ExitCommand { 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 #!/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/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp3.1/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/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/netcoreapp3.1/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.native/$1/lib/netstandard2.0/

4
src/Avalonia.Animation/Animation.cs

@ -251,10 +251,10 @@ namespace Avalonia.Animation
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) 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)); subscriptions.Add(newKF.BindSetter(setter, control));

9
src/Avalonia.Animation/AnimatorKeyFrame.cs

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

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

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

20
src/Avalonia.Animation/KeyFrame.cs

@ -19,6 +19,7 @@ namespace Avalonia.Animation
{ {
private TimeSpan _ktimeSpan; private TimeSpan _ktimeSpan;
private Cue _kCue; private Cue _kCue;
private KeySpline _kKeySpline;
public KeyFrame() 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, SystemIdle = 1,
/// <summary> /// <summary>
/// The job will be processed when the application sis idle. /// The job will be processed when the application is idle.
/// </summary> /// </summary>
ApplicationIdle = 2, ApplicationIdle = 2,

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

@ -55,7 +55,7 @@ namespace Avalonia.Controls
binding.Mode = BindingMode.TwoWay; binding.Mode = BindingMode.TwoWay;
} }
if (binding.Converter == null) if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat))
{ {
binding.Converter = DataGridValueConverter.Instance; 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, // Since we didn't know the final widths of the columns until we resized,
// we waited until now to measure each cell // we waited until now to measure each cell
double leftEdge = 0; double leftEdge = 0;
if (autoSizeHeight)
DesiredHeight = 0;
foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
{ {
DataGridCell cell = OwningRow.Cells[column.Index]; DataGridCell cell = OwningRow.Cells[column.Index];

7
src/Avalonia.Controls/AutoCompleteBox.cs

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

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

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

21
src/Avalonia.Controls/ComboBox.cs

@ -234,6 +234,23 @@ namespace Avalonia.Controls
base.OnTemplateApplied(e); 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) internal void ItemFocused(ComboBoxItem dropDownItem)
{ {
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) 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?.Dispose();
_subscriptionsOnOpen = null; _subscriptionsOnOpen = null;
PopupClosedOverride(e);
if (CanFocus(this)) if (CanFocus(this))
{ {
Focus(); 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.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItemBase> public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItemBase>, INativeMenuExporterEventsImplBridge
{ {
private readonly AvaloniaList<NativeMenuItemBase> _items = private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove }; new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
@ -17,12 +15,22 @@ namespace Avalonia.Controls
[Content] [Content]
public IList<NativeMenuItemBase> Items => _items; 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() public NativeMenu()
{ {
_items.Validate = Validator; _items.Validate = Validator;
_items.CollectionChanged += ItemsChanged; _items.CollectionChanged += ItemsChanged;
} }
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
Opening?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj) private void Validator(NativeMenuItemBase obj)
{ {
if (obj.Parent != null) if (obj.Parent != null)
@ -31,10 +39,10 @@ namespace Avalonia.Controls
private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
if(e.OldItems!=null) if (e.OldItems != null)
foreach (NativeMenuItemBase i in e.OldItems) foreach (NativeMenuItemBase i in e.OldItems)
i.Parent = null; i.Parent = null;
if(e.NewItems!=null) if (e.NewItems != null)
foreach (NativeMenuItemBase i in e.NewItems) foreach (NativeMenuItemBase i in e.NewItems)
i.Parent = this; i.Parent = this;
} }
@ -49,7 +57,7 @@ namespace Avalonia.Controls
} }
public void Add(NativeMenuItemBase item) => _items.Add(item); public void Add(NativeMenuItemBase item) => _items.Add(item);
public IEnumerator<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator(); public IEnumerator<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.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) 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;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public class NativeMenuItem : NativeMenuItemBase public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge
{ {
private string _header; private string _header;
private KeyGesture _gesture; 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; private NativeMenu _menu;
@ -55,13 +60,7 @@ namespace Avalonia.Controls
} }
public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty = public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o._menu, AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v);
(o, v) =>
{
if (v.Parent != null && v.Parent != o)
throw new InvalidOperationException("NativeMenu already has a parent");
o._menu = v;
});
public NativeMenu Menu 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 = 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 public string Header
{ {
get => GetValue(HeaderProperty); get => _header;
set => SetValue(HeaderProperty, value); set => SetAndRaise(HeaderProperty, ref _header, value);
} }
public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty = 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 public KeyGesture Gesture
{ {
get => GetValue(GestureProperty); get => _gesture;
set => SetValue(GestureProperty, value); 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 = public static readonly DirectProperty<NativeMenuItem, ICommand> CommandProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, ICommand>(nameof(Command), Button.CommandProperty.AddOwner<NativeMenuItem>(
o => o._command, (o, v) => menuItem => menuItem.Command,
{ (menuItem, command) => menuItem.Command = command,
if (o._command != null) enableDataValidation: true);
WeakSubscriptionManager.Unsubscribe(o._command,
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
o._command = v;
if (o._command != null)
WeakSubscriptionManager.Subscribe(o._command,
nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
o.CanExecuteChanged();
});
/// <summary> /// <summary>
/// Defines the <see cref="CommandParameter"/> property. /// Defines the <see cref="CommandParameter"/> property.
@ -114,27 +137,39 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object> CommandParameterProperty = public static readonly StyledProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>(); Button.CommandParameterProperty.AddOwner<MenuItem>();
public static readonly DirectProperty<NativeMenuItem, bool> EnabledProperty = public static readonly DirectProperty<NativeMenuItem, bool> IsEnabledProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(Enabled), o => o._enabled, AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true);
(o, v) => o._enabled = v, true);
public bool Enabled public bool IsEnabled
{ {
get => GetValue(EnabledProperty); get => _isEnabled;
set => SetValue(EnabledProperty, value); set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value);
} }
void CanExecuteChanged() void CanExecuteChanged()
{ {
Enabled = _command?.CanExecute(null) ?? true; IsEnabled = _command?.CanExecute(null) ?? true;
} }
public bool HasClickHandlers => Clicked != null; public bool HasClickHandlers => Clicked != null;
public ICommand Command public ICommand Command
{ {
get => GetValue(CommandProperty); get => _command;
set => SetValue(CommandProperty, value); 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> /// <summary>
@ -149,7 +184,7 @@ namespace Avalonia.Controls
public event EventHandler Clicked; public event EventHandler Clicked;
public void RaiseClick() void INativeMenuItemExporterEventsImplBridge.RaiseClicked()
{ {
Clicked?.Invoke(this, new EventArgs()); 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 System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Controls.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="dialog">The details of the file dialog to show.</param>
/// <param name="parent">The parent window.</param> /// <param name="parent">The parent window.</param>
/// <returns>A task returning the selected filenames.</returns> /// <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> /// <summary>
/// Raised when the popup closes. /// Raised when the popup closes.
/// </summary> /// </summary>
public event EventHandler? Closed; public event EventHandler<PopupClosedEventArgs>? Closed;
/// <summary> /// <summary>
/// Raised when the popup opens. /// Raised when the popup opens.
@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup) 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,
(x, handler) => x.Closed -= handler)); (x, handler) => x.Closed -= handler));
} }
@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Closes the popup. /// Closes the popup.
/// </summary> /// </summary>
public void Close() public void Close() => CloseCore(null);
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary> /// <summary>
/// Measures the control. /// 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) private void ListenForNonClientClick(RawInputEventArgs e)
{ {
var mouse = e as RawPointerEventArgs; var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{ {
Close(); CloseCore(e);
} }
} }
private void PointerPressedOutside(object sender, PointerPressedEventArgs e) private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{ {
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
{ {
Close(); CloseCore(e);
e.Handled = true;
} }
} }

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> protected override Size MeasureOverride(Size availableSize)
/// Carries out the arrange pass of the window. {
/// </summary> var measured = base.MeasureOverride(availableSize);
/// <param name="finalSize">The final window size.</param> var width = measured.Width;
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns> var height = measured.Height;
protected override Size ArrangeOverride(Size finalSize) 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()) using (BeginAutoSizing())
{ {
_positionerParameters.Size = finalSize; _positionerParameters.Size = size;
UpdatePosition(); 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.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -165,6 +166,14 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility), nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto); 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; internal const double DefaultSmallChange = 16;
private IDisposable _childSubscription; 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> /// <summary>
/// Gets the extent of the scrollable content. /// Gets the extent of the scrollable content.
/// </summary> /// </summary>
@ -203,9 +221,11 @@ namespace Avalonia.Controls
private set private set
{ {
var old = _extent;
if (SetAndRaise(ExtentProperty, ref _extent, value)) if (SetAndRaise(ExtentProperty, ref _extent, value))
{ {
CalculatedPropertiesChanged(); CalculatedPropertiesChanged(extentDelta: value - old);
} }
} }
} }
@ -222,11 +242,13 @@ namespace Avalonia.Controls
set set
{ {
var old = _offset;
value = ValidateOffset(this, value); value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value)) if (SetAndRaise(OffsetProperty, ref _offset, value))
{ {
CalculatedPropertiesChanged(); CalculatedPropertiesChanged(offsetDelta: value - old);
} }
} }
} }
@ -243,9 +265,11 @@ namespace Avalonia.Controls
private set private set
{ {
var old = _viewport;
if (SetAndRaise(ViewportProperty, ref _viewport, value)) 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, // 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. // 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(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport); 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) 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; using Avalonia.Media;
namespace Avalonia.Controls.Shapes namespace Avalonia.Controls.Shapes
@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
static Path() static Path()
{ {
AffectsGeometry<Path>(DataProperty); AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
} }
public Geometry Data public Geometry Data
@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
} }
protected override Geometry CreateDefiningGeometry() => Data; 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) if(parent == null)
throw new ArgumentNullException(nameof(parent)); throw new ArgumentNullException(nameof(parent));
return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>() return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
.ShowFileDialogAsync(this, parent?.PlatformImpl)) ?? .ShowFileDialogAsync(this, parent)) ??
Array.Empty<string>()).FirstOrDefault(); Array.Empty<string>()).FirstOrDefault();
} }
} }
@ -45,7 +45,7 @@ namespace Avalonia.Controls
{ {
if(parent == null) if(parent == null)
throw new ArgumentNullException(nameof(parent)); 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) if(parent == null)
throw new ArgumentNullException(nameof(parent)); 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(); } get { return GetSelection(); }
set set
{ {
if (value == null) _undoRedoHelper.Snapshot();
if (string.IsNullOrEmpty(value))
{ {
return; DeleteSelection();
} }
else
_undoRedoHelper.Snapshot(); {
HandleTextInput(value); HandleTextInput(value);
}
_undoRedoHelper.Snapshot(); _undoRedoHelper.Snapshot();
} }
} }
@ -471,8 +473,10 @@ namespace Avalonia.Controls
{ {
if (!IsPasswordBox) if (!IsPasswordBox)
{ {
_undoRedoHelper.Snapshot();
Copy(); Copy();
DeleteSelection(); DeleteSelection();
_undoRedoHelper.Snapshot();
} }
handled = true; handled = true;
@ -598,6 +602,7 @@ namespace Avalonia.Controls
break; break;
case Key.Back: case Key.Back:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlBackspace(); SetSelectionForControlBackspace();
@ -621,11 +626,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters; CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex; SelectionStart = SelectionEnd = CaretIndex;
} }
_undoRedoHelper.Snapshot();
handled = true; handled = true;
break; break;
case Key.Delete: case Key.Delete:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlDelete(); SetSelectionForControlDelete();
@ -647,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters)); text.Substring(caretIndex + removedCharacters));
} }
_undoRedoHelper.Snapshot();
handled = true; handled = true;
break; break;
@ -654,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter: case Key.Enter:
if (AcceptsReturn) if (AcceptsReturn)
{ {
_undoRedoHelper.Snapshot();
HandleTextInput(NewLine); HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
handled = true; handled = true;
} }
@ -663,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab: case Key.Tab:
if (AcceptsTab) if (AcceptsTab)
{ {
_undoRedoHelper.Snapshot();
HandleTextInput("\t"); HandleTextInput("\t");
_undoRedoHelper.Snapshot();
handled = true; handled = true;
} }
else else

12
src/Avalonia.Controls/TreeViewItem.cs

@ -51,6 +51,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty); SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true); FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e)); RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
} }
@ -179,5 +180,16 @@ namespace Avalonia.Controls
return logical != null ? result : @default; 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 /// Should be called from left mouse button press event handler
/// </summary> /// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); 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/> /// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
@ -450,6 +435,19 @@ namespace Avalonia.Controls
EnsureInitialized(); EnsureInitialized();
IsVisible = true; 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); LayoutManager.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing()) using (BeginAutoSizing())
@ -486,22 +484,12 @@ namespace Avalonia.Controls
/// <returns>. /// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes. /// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns> /// </returns>
public Task<TResult> ShowDialog<TResult>(Window owner) => ShowDialog<TResult>(owner.PlatformImpl); public Task<TResult> ShowDialog<TResult>(Window owner)
/// <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)
{ {
if (owner == null) if (owner == null)
{
throw new ArgumentNullException(nameof(owner)); throw new ArgumentNullException(nameof(owner));
}
if (IsVisible) if (IsVisible)
{ {
@ -512,29 +500,44 @@ namespace Avalonia.Controls
EnsureInitialized(); EnsureInitialized();
IsVisible = true; 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); LayoutManager.ExecuteInitialLayoutPass(this);
var result = new TaskCompletionSource<TResult>(); var result = new TaskCompletionSource<TResult>();
using (BeginAutoSizing()) using (BeginAutoSizing())
{ {
PlatformImpl?.ShowDialog(owner.PlatformImpl);
PlatformImpl?.ShowDialog(owner);
Renderer?.Start(); Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>( Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x, x => Closed += x,
x => this.Closed -= x) x => Closed -= x)
.Take(1) .Take(1)
.Subscribe(_ => .Subscribe(_ =>
{ {
owner.Activate(); owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult))); result.SetResult((TResult)(_dialogResult ?? default(TResult)));
}); });
OnOpened(EventArgs.Empty); OnOpened(EventArgs.Empty);
} }
SetWindowStartupLocation(owner); SetWindowStartupLocation(owner.PlatformImpl);
return result.Task; return result.Task;
} }
@ -569,38 +572,60 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var sizeToContent = SizeToContent; var sizeToContent = SizeToContent;
var clientSize = ClientSize; var clientSize = ClientSize;
var constraint = availableSize; var constraint = clientSize;
if ((sizeToContent & SizeToContent.Width) != 0) if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{ {
constraint = constraint.WithWidth(double.PositiveInfinity); constraint = constraint.WithWidth(double.PositiveInfinity);
} }
if ((sizeToContent & SizeToContent.Height) != 0) if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{ {
constraint = constraint.WithHeight(double.PositiveInfinity); constraint = constraint.WithHeight(double.PositiveInfinity);
} }
var result = base.MeasureOverride(constraint); 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; return result;
} }
protected sealed override Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(size);
return ClientSize;
}
}
protected sealed override void HandleClosed() protected sealed override void HandleClosed()
{ {
RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
@ -616,6 +641,9 @@ namespace Avalonia.Controls
SizeToContent = SizeToContent.Manual; SizeToContent = SizeToContent.Manual;
} }
Width = clientSize.Width;
Height = clientSize.Height;
base.HandleResized(clientSize); base.HandleResized(clientSize);
} }

48
src/Avalonia.Controls/WindowBase.cs

@ -224,16 +224,54 @@ namespace Avalonia.Controls
/// <param name="clientSize">The new client size.</param> /// <param name="clientSize">The new client size.</param>
protected override void HandleResized(Size clientSize) protected override void HandleResized(Size clientSize)
{ {
if (!AutoSizing)
{
Width = clientSize.Width;
Height = clientSize.Height;
}
ClientSize = clientSize; ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass(); LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize); 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> /// <summary>
/// Handles a window position change notification from /// Handles a window position change notification from
/// <see cref="IWindowBaseImpl.PositionChanged"/>. /// <see cref="IWindowBaseImpl.PositionChanged"/>.

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

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

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

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

2
src/Avalonia.Dialogs/ManagedFileChooser.xaml

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

12
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,19 +1,15 @@
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.Platform;
namespace Avalonia.Dialogs namespace Avalonia.Dialogs
{ {
public static class ManagedFileDialogExtensions 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); var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
@ -39,12 +35,12 @@ namespace Avalonia.Dialogs
return result; 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); 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(); return (await Show(dialog, parent))?.FirstOrDefault();
} }

35
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.IO;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop
private static string[] AllProperties = new[] 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) object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
@ -210,7 +211,7 @@ namespace Avalonia.FreeDesktop
return null; return null;
if (item.Menu != null && item.Menu.Items.Count == 0) if (item.Menu != null && item.Menu.Items.Count == 0)
return false; return false;
if (item.Enabled == false) if (item.IsEnabled == false)
return false; return false;
return null; return null;
} }
@ -234,6 +235,30 @@ namespace Avalonia.FreeDesktop
return new[] { lst.ToArray() }; 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") if (name == "children-display")
return menu != null ? "submenu" : null; return menu != null ? "submenu" : null;
} }
@ -319,10 +344,10 @@ namespace Avalonia.FreeDesktop
{ {
var item = GetMenu(id).item; var item = GetMenu(id).item;
if (item is NativeMenuItem menuItem) if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
{ {
if (menuItem?.Enabled == true) if (menuItem?.IsEnabled == true)
menuItem.RaiseClick(); bridge?.RaiseClicked();
} }
} }
} }

14
src/Avalonia.Input/FocusManager.cs

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

6
src/Avalonia.Input/IFocusManager.cs

@ -20,11 +20,11 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="control">The control to focus.</param> /// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</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( void Focus(
IInputElement control, IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None); KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary> /// <summary>
/// Notifies the focus manager of a change in focus scope. /// 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( void SetFocusedElement(
IInputElement element, IInputElement element,
NavigationMethod method, NavigationMethod method,
InputModifiers modifiers); KeyModifiers modifiers);
} }
} }

6
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -19,10 +19,10 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</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( void Move(
IInputElement element, IInputElement element,
NavigationDirection direction, 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( public void SetFocusedElement(
IInputElement element, IInputElement element,
NavigationMethod method, NavigationMethod method,
InputModifiers modifiers) KeyModifiers keyModifiers)
{ {
if (element != FocusedElement) if (element != FocusedElement)
{ {
@ -53,7 +53,7 @@ namespace Avalonia.Input
{ {
RoutedEvent = InputElement.GotFocusEvent, RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method, NavigationMethod = method,
InputModifiers = modifiers, KeyModifiers = keyModifiers,
}); });
} }
} }

8
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -91,11 +91,11 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</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( public void Move(
IInputElement element, IInputElement element,
NavigationDirection direction, NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(element != null); Contract.Requires<ArgumentNullException>(element != null);
@ -106,7 +106,7 @@ namespace Avalonia.Input
var method = direction == NavigationDirection.Next || var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ? direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional; 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 ? var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous; NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.Modifiers); Move(current, direction, e.KeyModifiers);
e.Handled = true; e.Handled = true;
} }
} }

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

@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
/// <summary> /// <summary>
/// Gets the type of the event. /// Gets the type of the event.
/// </summary> /// </summary>
public RawPointerEventType Type { get; private set; } public RawPointerEventType Type { get; set; }
/// <summary> /// <summary>
/// Gets the input modifiers. /// 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 /// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
/// this method in a derived class. /// this method in a derived class.
/// </remarks> /// </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> /// <summary>
/// Removes any state the layout previously stored on the ILayoutable container. /// 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 /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </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> /// <summary>
/// Suggests a DesiredSize for a container element. A container element that supports /// 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. /// if scrolling or other resize behavior is possible in that particular container.
/// </param> /// </param>
/// <returns></returns> /// <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> /// <summary>
/// Positions child elements and determines a size for a container UIElement. Container /// 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. /// The final size that the container computes for the child in layout.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <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> /// <summary>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference /// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously. /// occurs asynchronously.
/// </summary> /// </summary>
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); 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 System;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Layout namespace Avalonia.Layout
@ -19,16 +20,11 @@ namespace Avalonia.Layout
/// <returns>The control's size.</returns> /// <returns>The control's size.</returns>
public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
{ {
var controlWidth = control.Width; var minmax = new MinMax(control);
var controlHeight = control.Height;
return new Size(
double width = (controlWidth > 0) ? controlWidth : constraints.Width; MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
double height = (controlHeight > 0) ? controlHeight : constraints.Height; MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
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);
} }
public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding, public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding,
@ -85,5 +81,39 @@ namespace Avalonia.Layout
InnerInvalidateMeasure(control); 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> /// </remarks>
public abstract class NonVirtualizingLayout : AttachedLayout 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> /// <summary>
/// When overridden in a derived class, initializes any per-container state the layout /// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container. /// 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 /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </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 /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </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 /// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size. /// fixed container size.
/// </returns> /// </returns>
protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary> /// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children. /// its children.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <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. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
/// <summary> /// <summary>
@ -10,5 +12,20 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public abstract class NonVirtualizingLayoutContext : LayoutContext 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, }; 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 state = context.LayoutState;
var stackState = state as StackLayoutState; var stackState = state as StackLayoutState;
@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this); stackState.InitializeForContext(context, this);
} }
protected override void UninitializeForContextCore(VirtualizingLayoutContext context) protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{ {
var stackState = (StackLayoutState)context.LayoutState; var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context); 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( var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize, availableSize,
@ -275,7 +275,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height); 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( var value = GetFlowAlgorithm(context).Arrange(
finalSize, 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 state = context.LayoutState;
var gridState = state as UniformGridLayoutState; var gridState = state as UniformGridLayoutState;
@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this); gridState.InitializeForContext(context, this);
} }
protected override void UninitializeForContextCore(VirtualizingLayoutContext context) protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{ {
var gridState = (UniformGridLayoutState)context.LayoutState; var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context); 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. // 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. // 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); 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( var value = GetFlowAlgorithm(context).Arrange(
finalSize, 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> /// </remarks>
public abstract class VirtualizingLayout : AttachedLayout 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> /// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items) /// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed. /// has changed.
@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </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 /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </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 /// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size. /// fixed container size.
/// </returns> /// </returns>
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary> /// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children. /// its children.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <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> /// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items) /// 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> /// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext public abstract class VirtualizingLayoutContext : LayoutContext
{ {
private NonVirtualizingLayoutContext _contextAdapter;
/// <summary> /// <summary>
/// Gets the number of items in the data. /// Gets the number of items in the data.
/// </summary> /// </summary>
@ -186,5 +188,8 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
/// <param name="element">The element to clear.</param> /// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(ILayoutable element); 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;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Dialogs;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Dialogs;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Native 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 class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
{ {
private IAvaloniaNativeFactory _factory; private IAvaloniaNativeFactory _factory;
private NativeMenu _menu;
private bool _resetQueued; private bool _resetQueued;
private bool _exported = false; private bool _exported = false;
private IAvnWindow _nativeWindow; private IAvnWindow _nativeWindow;
private List<NativeMenuItem> _menuItems = new List<NativeMenuItem>(); private NativeMenu _menu;
private IAvnMenu _nativeMenu;
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 },
};
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
{ {
@ -193,7 +29,6 @@ namespace Avalonia.Native
{ {
_factory = factory; _factory = factory;
_menu = NativeMenu.GetMenu(Application.Current);
DoLayoutReset(); DoLayoutReset();
} }
@ -203,17 +38,19 @@ namespace Avalonia.Native
public void SetNativeMenu(NativeMenu menu) public void SetNativeMenu(NativeMenu menu)
{ {
if (menu == null) _menu = menu == null ? new NativeMenu() : menu;
menu = new NativeMenu();
if (_menu != null)
((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
_menu = menu;
((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
DoLayoutReset(); DoLayoutReset();
} }
internal void UpdateIfNeeded()
{
if (_resetQueued)
{
DoLayoutReset();
}
}
private static NativeMenu CreateDefaultAppMenu() private static NativeMenu CreateDefaultAppMenu()
{ {
var result = new NativeMenu(); var result = new NativeMenu();
@ -237,50 +74,34 @@ namespace Avalonia.Native
return result; return result;
} }
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
QueueReset();
}
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
QueueReset();
}
void DoLayoutReset() void DoLayoutReset()
{ {
_resetQueued = false; _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) if (appMenu == null)
{
SetMenu(_menu);
}
else
{ {
SetMenu(CreateDefaultAppMenu()); appMenu = CreateDefaultAppMenu();
NativeMenu.SetMenu(Application.Current, appMenu);
} }
SetMenu(appMenu);
} }
else else
{ {
SetMenu(_nativeWindow, _menu?.Items); if (_menu != null)
{
SetMenu(_nativeWindow, _menu);
}
} }
_exported = true; _exported = true;
} }
private void QueueReset() internal void QueueReset()
{ {
if (_resetQueued) if (_resetQueued)
return; return;
@ -288,188 +109,64 @@ namespace Avalonia.Native
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
} }
private IAvnAppMenu CreateSubmenu(ICollection<NativeMenuItemBase> children) private void SetMenu(NativeMenu menu)
{ {
var menu = _factory.CreateMenu(); var menuItem = menu.Parent;
SetChildren(menu, children);
return menu; var appMenuHolder = menuItem?.Parent;
}
private void AddMenuItem(NativeMenuItem item) if (menu.Parent is null)
{
if (item.Menu?.Items != null)
{ {
((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; menuItem = new NativeMenuItem();
} }
}
private static string ConvertOSXSpecialKeyCodes(Key key) if (appMenuHolder is null)
{
if (osxKeys.ContainsKey(key))
{
return ((char)osxKeys[key]).ToString();
}
else
{ {
return key.ToString().ToLower(); appMenuHolder = new NativeMenu();
}
}
private void SetChildren(IAvnAppMenu menu, ICollection<NativeMenuItemBase> children) appMenuHolder.Add(menuItem);
{
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());
}
} }
}
private void AddItemsToMenu(IAvnAppMenu menu, ICollection<NativeMenuItemBase> items, bool isMainMenu = false) menuItem.Menu = menu;
{
foreach (var i in items)
{
if (i is NativeMenuItem item)
{
var menuItem = _factory.CreateMenuItem();
AddMenuItem(item);
menuItem.SetAction(new PredicateCallback(() =>
{
if (item.Command != null || item.HasClickHandlers)
{
return item.Enabled;
}
return false;
}), new MenuActionCallback(() => { item.RaiseClick(); }));
if (item.Menu?.Items.Count >= 0 || isMainMenu)
{
var subMenu = CreateSubmenu(item.Menu?.Items);
menuItem.SetSubMenu(subMenu);
using (var buffer = new Utf8Buffer(item.Header))
{
subMenu.Title = buffer.DangerousGetHandle();
}
}
else
{
using (var buffer = new Utf8Buffer(item.Header))
{
menuItem.Title = buffer.DangerousGetHandle();
}
if (item.Gesture != null)
{
using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower()))
{
menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers);
}
}
}
menu.AddItem(menuItem);
}
else if(i is NativeMenuItemSeperator seperator)
{
menu.AddItem(_factory.CreateMenuItemSeperator());
}
}
}
private void SetMenu(NativeMenu menu) var setMenu = false;
{
var appMenu = _factory.ObtainAppMenu();
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) setMenu = true;
{
menuItem = new NativeMenuItem();
} }
menuItem.Menu = menu; _nativeMenu.Update(_factory, appMenuHolder);
appMenu.Clear();
AddItemsToMenu(appMenu, new List<NativeMenuItemBase> { menuItem });
_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) setMenu = true;
{
appMenu = _factory.CreateMenu();
} }
appMenu.Clear(); _nativeMenu.Update(_factory, menu);
AddItemsToMenu(appMenu, menuItems);
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=".*::.*::ppv" return="true"/>
<map param=".*::.*::ret" return="true"/> <map param=".*::.*::ret" return="true"/>
<map param=".*::.*::retOut" attribute="out" return="true"/> <map param=".*::.*::retOut" attribute="out" return="true"/>
<map method="IAvnAppMenu:.*" visibility="private" />
<map method="IAvnAppMenuItem:.*" visibility="private" />
</mapping> </mapping>
</config> </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;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform;
namespace Avalonia.Native namespace Avalonia.Native
{ {
@ -18,13 +17,15 @@ namespace Avalonia.Native
_native = native; _native = native;
} }
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{ {
var events = new SystemDialogEvents(); var events = new SystemDialogEvents();
var nativeParent = GetNativeWindow(parent);
if (dialog is OpenFileDialog ofd) if (dialog is OpenFileDialog ofd)
{ {
_native.OpenFileDialog((parent as WindowImpl)?.Native, _native.OpenFileDialog(nativeParent,
events, ofd.AllowMultiple, events, ofd.AllowMultiple,
ofd.Title ?? "", ofd.Title ?? "",
ofd.InitialDirectory ?? "", ofd.InitialDirectory ?? "",
@ -33,7 +34,7 @@ namespace Avalonia.Native
} }
else else
{ {
_native.SaveFileDialog((parent as WindowImpl)?.Native, _native.SaveFileDialog(nativeParent,
events, events,
dialog.Title ?? "", dialog.Title ?? "",
dialog.InitialDirectory ?? "", dialog.InitialDirectory ?? "",
@ -44,14 +45,21 @@ namespace Avalonia.Native
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); 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(); 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(); }); 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 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) public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{ {
return builder.AfterSetup(_ => return builder.AfterPlatformServicesSetup(_ =>
{ {
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));

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

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName); _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
} }
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>(); 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); current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily; fontFamily = _defaultFontFamily;
} }
var key = new FontKey(fontFamily, fontWeight, fontStyle); var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface)) if (_typefaceCache.TryGetValue(key, out var typeface))
{ {
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal, FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null) FontFamily fontFamily = null, CultureInfo culture = null)
{ {
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? foreach (var cachedTypeface in _typefaceCache.Values)
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : {
// 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; null;
return matchedTypeface;
} }
} }
} }

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

@ -1,7 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames)); throw new ArgumentNullException(nameof(familyNames));
} }
Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0]; 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 struct FontKey : IEquatable<FontKey>
{ {
public readonly FontFamily FontFamily; public FontKey(string familyName, FontWeight weight, FontStyle style)
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{ {
FontFamily = fontFamily; FamilyName = familyName;
Style = style; Style = style;
Weight = weight; Weight = weight;
} }
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode() public override int GetHashCode()
{ {
var hash = FontFamily.GetHashCode(); var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight; hash = hash * 31 + (int)Weight;
@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other) public bool Equals(FontKey other)
{ {
return FontFamily == other.FontFamily && return FamilyName == other.FamilyName &&
Style == other.Style && Style == other.Style &&
Weight == other.Weight; Weight == other.Weight;
} }

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

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
currentTypeface = 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)) 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 namespace Avalonia.Media.TextFormatting.Unicode
{ {
internal ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private ReadOnlySlice<char> _text; 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); gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null) if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName)) 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); gtk_window_present(dlg);
return tcs.Task; return tcs.Task;
} }
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{ {
await EnsureInitialized(); await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread( return await await RunOnGlibThread(
() => ShowDialog(dialog.Title, parent, () => ShowDialog(dialog.Title, platformImpl,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false, (dialog as OpenFileDialog)?.AllowMultiple ?? false,
Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters)); 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(); await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(async () => 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); GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null);
return res?.FirstOrDefault(); return res?.FirstOrDefault();
}); });

10
src/Avalonia.X11/X11ImmediateRendererProxy.cs

@ -11,6 +11,7 @@ namespace Avalonia.X11
private readonly IRenderLoop _loop; private readonly IRenderLoop _loop;
private ImmediateRenderer _renderer; private ImmediateRenderer _renderer;
private bool _invalidated; private bool _invalidated;
private bool _running;
private object _lock = new object(); private object _lock = new object();
public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop) public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop)
@ -22,6 +23,7 @@ namespace Avalonia.X11
public void Dispose() public void Dispose()
{ {
_running = false;
_renderer.Dispose(); _renderer.Dispose();
} }
@ -78,12 +80,14 @@ namespace Avalonia.X11
public void Start() public void Start()
{ {
_running = true;
_loop.Add(this); _loop.Add(this);
_renderer.Start(); _renderer.Start();
} }
public void Stop() public void Stop()
{ {
_running = false;
_loop.Remove(this); _loop.Remove(this);
_renderer.Stop(); _renderer.Stop();
} }
@ -100,7 +104,11 @@ namespace Avalonia.X11
{ {
lock (_lock) lock (_lock)
_invalidated = false; _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(); updatedSizeViaScaling = UpdateScaling();
} }
if (changedSize && !updatedSizeViaScaling) if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize); Resized?.Invoke(ClientSize);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
@ -649,7 +649,27 @@ namespace Avalonia.X11
ScheduleInput(args); 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) if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling; mouse.Position = mouse.Position / Scaling;

10
src/Avalonia.X11/XI2Manager.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ? (ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate : RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd); 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)); ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return; return;
} }
@ -230,10 +230,10 @@ namespace Avalonia.X11
} }
if (scrollDelta != default) 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)); client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev)) 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)); RawPointerEventType.Move, ev.Position, ev.Modifiers));
} }
@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null _ => (RawPointerEventType?)null
}; };
if (type.HasValue) 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)); type.Value, ev.Position, ev.Modifiers));
} }
@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client interface IXI2Client
{ {
IInputRoot InputRoot { get; } IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args); void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; } IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { 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 namespace Avalonia.Markup.Xaml.Converters
{ {
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Utilities;
public class PointsListTypeConverter : TypeConverter public class PointsListTypeConverter : TypeConverter
{ {
@ -15,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{ {
string strValue = (string)value; var points = new List<Point>();
string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var result = new List<Point>(pointStrs.Length); using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList."))
foreach (var pointStr in pointStrs)
{ {
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)); => AddType(typeSystem.GetType(type), typeSystem.GetType(conv));
Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); 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"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); 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; 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) protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func<IXamlIlMethod, bool> method)
{ {
var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors"); var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors");
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m));
m.Parameters[0].FullName == "Avalonia.Styling.Selector"
&& method(m));
codeGen.EmitCall(found); codeGen.EmitCall(found);
} }
} }
@ -308,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
_selectors.Add(node); _selectors.Add(node);
} }
//TODO: actually find the type public override IXamlIlType TargetType
public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.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) protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
{ {
if (_selectors.Count == 0) if (_selectors.Count == 0)

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

@ -30,6 +30,10 @@ namespace Avalonia.Skia
private Matrix _currentTransform; private Matrix _currentTransform;
private GRContext _grContext; private GRContext _grContext;
private bool _disposed; private bool _disposed;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
/// <summary> /// <summary>
/// Context create info. /// Context create info.
/// </summary> /// </summary>
@ -153,7 +157,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2) 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); 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 impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size; var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{ {
if (fill.Paint != null) if (fill.Paint != null)
{ {
@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null) if (brush != null)
{ {
using (var paint = CreatePaint(brush, rect.Size)) using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null) if (pen?.Brush != null)
{ {
using (var paint = CreatePaint(pen, rect.Size)) using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -222,7 +226,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) 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; var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -232,14 +236,14 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) 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; var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint); ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, 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(); var paint = new SKPaint();
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(mask, bounds.Size)); _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
} }
/// <inheritdoc /> /// <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> /// <summary>
/// Configure paint wrapper for using gradient brush. /// Configure paint wrapper for using gradient brush.
/// </summary> /// </summary>
@ -514,17 +527,16 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given brush. /// Creates paint wrapper for given brush.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param> /// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns> /// <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 var paintWrapper = new PaintWrapper(paint, disposePaint);
{
IsAntialias = true
};
var paintWrapper = new PaintWrapper(paint); paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity; double opacity = brush.Opacity * _currentOpacity;
@ -572,10 +584,12 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given pen. /// Creates paint wrapper for given pen.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param> /// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns> /// <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 // In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered. // and for us it means - there is nothing rendered.
@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default; return default;
} }
var rv = CreatePaint(pen.Brush, targetSize); var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
var paint = rv.Paint;
paint.IsStroke = true; paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness; paint.StrokeWidth = (float) pen.Thickness;
@ -668,7 +681,7 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Skia cached paint state. /// Skia cached paint state.
/// </summary> /// </summary>
private struct PaintState : IDisposable private readonly struct PaintState : IDisposable
{ {
private readonly SKColor _color; private readonly SKColor _color;
private readonly SKShader _shader; private readonly SKShader _shader;
@ -696,14 +709,16 @@ namespace Avalonia.Skia
{ {
//We are saving memory allocations there //We are saving memory allocations there
public readonly SKPaint Paint; public readonly SKPaint Paint;
private readonly bool _disposePaint;
private IDisposable _disposable1; private IDisposable _disposable1;
private IDisposable _disposable2; private IDisposable _disposable2;
private IDisposable _disposable3; private IDisposable _disposable3;
public PaintWrapper(SKPaint paint) public PaintWrapper(SKPaint paint, bool disposePaint)
{ {
Paint = paint; Paint = paint;
_disposePaint = disposePaint;
_disposable1 = null; _disposable1 = null;
_disposable2 = null; _disposable2 = null;
@ -751,7 +766,15 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
Paint?.Dispose(); if (_disposePaint)
{
Paint?.Dispose();
}
else
{
Paint?.Reset();
}
_disposable1?.Dispose(); _disposable1?.Dispose();
_disposable2?.Dispose(); _disposable2?.Dispose();
_disposable3?.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, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) 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) if (culture == null)
{ {
culture = CultureInfo.CurrentUICulture; culture = CultureInfo.CurrentUICulture;
@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; 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, var skTypeface =
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null) if (skTypeface == null)
{ {
continue; continue;
} }
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
} }
else else
{ {
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null) if (skTypeface != null)
{ {
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{ {
var skTypeface = SKTypeface.Default; SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null) if (typeface.FontFamily.Key == null)
{ {
@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface); skTypeface = fontCollection.Get(typeface);
} }
if (skTypeface == null)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
return new GlyphTypefaceImpl(skTypeface); return new GlyphTypefaceImpl(skTypeface);
} }
} }

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

@ -140,25 +140,17 @@ namespace Avalonia.Skia
public Rect HitTestTextPosition(int index) 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(); var rects = GetRects();
if (index >= Text.Length || index < 0)
if (index < 0 || index >= rects.Count)
{ {
var r = rects.LastOrDefault(); var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); 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]; return rects[index];
} }
@ -274,7 +266,8 @@ namespace Avalonia.Skia
if (fb != null) if (fb != null)
{ {
//TODO: figure out how to get the brush size //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 else
{ {

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

@ -7,17 +7,11 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl public class GlyphRunImpl : IGlyphRunImpl
{ {
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) public GlyphRunImpl(SKTextBlob textBlob)
{ {
Paint = paint;
TextBlob = textBlob; TextBlob = textBlob;
} }
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary> /// <summary>
/// Gets the text blob to draw. /// Gets the text blob to draw.
/// </summary> /// </summary>
@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
TextBlob.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); 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 /> /// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{ {
@ -158,92 +168,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface; var typeface = glyphTypeface.Typeface;
var paint = new SKPaint s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
{ s_paint.Typeface = typeface;
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); SKTextBlob textBlob;
if (glyphRun.GlyphOffsets.IsEmpty) width = 0;
{
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
textBlob = textBlobBuilder.Build(); var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; if (glyphRun.GlyphOffsets.IsEmpty)
} {
else if (glyphTypeface.IsFixedPitch)
{ {
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
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];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); textBlob = s_textBlobBuilder.Build();
textBlob = textBlobBuilder.Build(); width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
} }
else else
{ {
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
var glyphPositions = buffer.GetPositionSpan(); var positions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var glyphOffset = glyphRun.GlyphOffsets[i]; positions[i] = (float)width;
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty) if (glyphRun.GlyphAdvances.IsEmpty)
{ {
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
} }
else else
{ {
currentX += glyphRun.GlyphAdvances[i]; width += glyphRun.GlyphAdvances[i];
} }
} }
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); 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) 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); return GetNearestMatch(_typefaces, key);
} }
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0) if (keys.Length == 0)
{ {
return SKTypeface.Default; return null;
} }
key = keys[0]; key = keys[0];

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

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue; 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); typeFaceCollection.AddTypeface(key, typeface);
} }

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

Loading…
Cancel
Save