Browse Source

Merge branch 'master' into captionbuttons-api

Max Katz 4 years ago
parent
commit
88a8c090c3
100 changed files with 2161 additions and 617 deletions
  1. 1 1
      build/ReactiveUI.props
  2. 1 0
      native/Avalonia.Native/src/OSX/AvnString.h
  3. 15 0
      native/Avalonia.Native/src/OSX/AvnString.mm
  4. 25 3
      native/Avalonia.Native/src/OSX/app.mm
  5. 1 1
      native/Avalonia.Native/src/OSX/clipboard.mm
  6. 4 2
      native/Avalonia.Native/src/OSX/common.h
  7. 21 4
      native/Avalonia.Native/src/OSX/main.mm
  8. 5 4
      native/Avalonia.Native/src/OSX/menu.h
  9. 74 45
      native/Avalonia.Native/src/OSX/menu.mm
  10. 11 3
      native/Avalonia.Native/src/OSX/window.mm
  11. 0 4
      nukebuild/Build.cs
  12. 1 1
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  13. 1 1
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  14. 2 2
      samples/ControlCatalog/MainWindow.xaml
  15. 0 47
      src/Android/Avalonia.Android/ActivityTracker.cs
  16. 7 25
      src/Android/Avalonia.Android/AndroidPlatform.cs
  17. 8 7
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  18. 1 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  19. 3 8
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  20. 34 1
      src/Android/Avalonia.Android/AvaloniaView.cs
  21. 101 0
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  22. 13 4
      src/Android/Avalonia.Android/CursorFactory.cs
  23. 2 4
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  24. 10 3
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  25. 0 14
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  26. 4 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  27. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  28. 2 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  29. 18 30
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  30. 22 69
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  31. 0 11
      src/Android/Avalonia.Android/app.config
  32. 2 2
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  33. 1 1
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  34. 24 4
      src/Avalonia.Animation/Animatable.cs
  35. 8 8
      src/Avalonia.Animation/Animation.cs
  36. 39 1
      src/Avalonia.Base/AvaloniaObject.cs
  37. 6 0
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  38. 53 7
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  39. 35 4
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  40. 8 0
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  41. 8 1
      src/Avalonia.Base/PropertyStore/IValue.cs
  42. 16 0
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  43. 95 26
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  44. 5 14
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  45. 259 42
      src/Avalonia.Base/ValueStore.cs
  46. 12 0
      src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
  47. 2 2
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  48. 9 2
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  49. 35 21
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  50. 2 0
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  51. 5 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  52. 9 2
      src/Avalonia.Controls/Application.cs
  53. 14 0
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  54. 5 3
      src/Avalonia.Controls/AutoCompleteBox.cs
  55. 1 0
      src/Avalonia.Controls/Button.cs
  56. 70 0
      src/Avalonia.Controls/ComboBox.cs
  57. 29 40
      src/Avalonia.Controls/Control.cs
  58. 55 22
      src/Avalonia.Controls/DefinitionBase.cs
  59. 52 3
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  60. 2 0
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  61. 4 18
      src/Avalonia.Controls/NativeControlHost.cs
  62. 32 1
      src/Avalonia.Controls/NativeMenu.cs
  63. 16 0
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  64. 0 10
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  65. 12 5
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  66. 16 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  67. 1 1
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  68. 7 0
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  69. 18 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  70. 3 1
      src/Avalonia.Controls/Slider.cs
  71. 29 14
      src/Avalonia.Controls/TextBox.cs
  72. 14 0
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  73. 25 20
      src/Avalonia.Controls/Window.cs
  74. 1 1
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  75. 21 0
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  76. 225 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  77. 8 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  78. 51 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
  79. 27 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  80. 59 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  81. 43 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
  82. 7 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  83. 121 3
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  84. 10 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  85. 1 1
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  86. 1 1
      src/Avalonia.Layout/ElementManager.cs
  87. 1 1
      src/Avalonia.Layout/UniformGridLayout.cs
  88. 5 5
      src/Avalonia.Layout/UniformGridLayoutState.cs
  89. 14 0
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  90. 6 3
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  91. 2 0
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  92. 24 2
      src/Avalonia.Native/IAvnMenu.cs
  93. 12 6
      src/Avalonia.Native/avn.idl
  94. 11 7
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  95. 4 0
      src/Avalonia.Styling/ApiCompatBaseline.txt
  96. 21 0
      src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs
  97. 17 0
      src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs
  98. 3 1
      src/Avalonia.Styling/IStyledElement.cs
  99. 35 5
      src/Avalonia.Styling/StyledElement.cs
  100. 5 0
      src/Avalonia.Styling/Styling/IStyleInstance.cs

+ 1 - 1
build/ReactiveUI.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="ReactiveUI" Version="12.1.1" />
+    <PackageReference Include="ReactiveUI" Version="13.2.10" />
   </ItemGroup>
 </Project>

+ 1 - 0
native/Avalonia.Native/src/OSX/AvnString.h

@@ -11,6 +11,7 @@
 
 extern IAvnString* CreateAvnString(NSString* string);
 extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
+extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
 extern IAvnStringArray* CreateAvnStringArray(NSString* string);
 extern IAvnString* CreateByteArray(void* data, int len);
 #endif /* AvnString_h */

+ 15 - 0
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -85,6 +85,16 @@ public:
         }
     }
     
+    AvnStringArrayImpl(NSArray<NSURL*>* array)
+    {
+        for(int c = 0; c < [array count]; c++)
+        {
+            ComPtr<IAvnString> s;
+            *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
+            _list.push_back(s);
+        }
+    }
+    
     AvnStringArrayImpl(NSString* string)
     {
         ComPtr<IAvnString> s;
@@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
     return new AvnStringArrayImpl(array);
 }
 
+IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*> * array)
+{
+    return new AvnStringArrayImpl(array);
+}
+
 IAvnStringArray* CreateAvnStringArray(NSString* string)
 {
     return new AvnStringArrayImpl(string);

+ 25 - 3
native/Avalonia.Native/src/OSX/app.mm

@@ -1,10 +1,20 @@
 #include "common.h"
+#include "AvnString.h"
 @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
+-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
 @end
 
 NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
 
 @implementation AvnAppDelegate
+ComPtr<IAvnApplicationEvents> _events;
+
+- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events
+{
+    _events = events;
+    return self;
+}
+
 - (void)applicationWillFinishLaunching:(NSNotification *)notification
 {
     if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
     [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
 }
 
+- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
+{
+    auto array = CreateAvnStringArray(filenames);
+    
+    _events->FilesOpened(array);
+}
+
+- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
+{
+    auto array = CreateAvnStringArray(urls);
+    
+    _events->FilesOpened(array);
+}
 @end
 
 @interface AvnApplication : NSApplication
 
-
 @end
 
 @implementation AvnApplication
@@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
 
 @end
 
-extern void InitializeAvnApp()
+extern void InitializeAvnApp(IAvnApplicationEvents* events)
 {
     NSApplication* app = [AvnApplication sharedApplication];
-    id delegate = [AvnAppDelegate new];
+    id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
     [app setDelegate:delegate];
 }

+ 1 - 1
native/Avalonia.Native/src/OSX/clipboard.mm

@@ -56,7 +56,7 @@ public:
                 return S_OK;
             }
             
-            NSArray* arr = (NSArray*)data;
+            NSArray<NSString*>* arr = (NSArray*)data;
             
             for(int c = 0; c < [arr count]; c++)
                 if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])

+ 4 - 2
native/Avalonia.Native/src/OSX/common.h

@@ -23,13 +23,15 @@ extern IAvnCursorFactory* CreateCursorFactory();
 extern IAvnGlDisplay* GetGlDisplay();
 extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
 extern IAvnMenuItem* CreateAppMenuItem();
-extern IAvnMenuItem* CreateAppMenuItemSeperator();
+extern IAvnMenuItem* CreateAppMenuItemSeparator();
 extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
 extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
 extern IAvnMenu* GetAppMenu ();
 extern NSMenuItem* GetAppMenuItem ();
+extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
+extern bool GetAutoGenerateDefaultAppMenuItems ();
 
-extern void InitializeAvnApp();
+extern void InitializeAvnApp(IAvnApplicationEvents* events);
 extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
 extern NSPoint ToNSPoint (AvnPoint p);
 extern AvnPoint ToAvnPoint (NSPoint p);

+ 21 - 4
native/Avalonia.Native/src/OSX/main.mm

@@ -2,6 +2,7 @@
 #define COM_GUIDS_MATERIALIZE
 #include "common.h"
 
+static bool s_generateDefaultAppMenuItems = true;
 static NSString* s_appTitle = @"Avalonia";
 
 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
@@ -122,6 +123,12 @@ public:
             ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory;
         return S_OK;
     }
+    
+    virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override
+    {
+        SetAutoGenerateDefaultAppMenuItems(!enabled);
+        return S_OK;
+    }
 };
 
 /// See "Using POSIX Threads in a Cocoa Application" section here:
@@ -156,13 +163,13 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
     
 public:
     FORWARD_IUNKNOWN()
-    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
+    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override
     {
         _deallocator = deallocator;
         @autoreleasepool{
             [[ThreadingInitializer new] do];
         }
-        InitializeAvnApp();
+        InitializeAvnApp(events);
         return S_OK;
     };
     
@@ -246,9 +253,9 @@ public:
         return S_OK;
     }
     
-    virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
+    virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override
     {
-        *ppv = ::CreateAppMenuItemSeperator();
+        *ppv = ::CreateAppMenuItemSeparator();
         return S_OK;
     }
     
@@ -310,3 +317,13 @@ CGFloat PrimaryDisplayHeight()
 {
   return NSMaxY([[[NSScreen screens] firstObject] frame]);
 }
+
+void SetAutoGenerateDefaultAppMenuItems (bool enabled)
+{
+    s_generateDefaultAppMenuItems = enabled;
+}
+
+bool GetAutoGenerateDefaultAppMenuItems ()
+{
+    return s_generateDefaultAppMenuItems;
+}

+ 5 - 4
native/Avalonia.Native/src/OSX/menu.h

@@ -31,13 +31,13 @@ private:
     NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
     IAvnActionCallback* _callback;
     IAvnPredicateCallback* _predicate;
-    bool _isSeperator;
+    bool _isSeparator;
     bool _isCheckable;
     
 public:
     FORWARD_IUNKNOWN()
     
-    AvnAppMenuItem(bool isSeperator);
+    AvnAppMenuItem(bool isSeparator);
     
     NSMenuItem* GetNative();
     
@@ -60,7 +60,6 @@ public:
     void RaiseOnClicked();
 };
 
-
 class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
 {
 private:
@@ -71,10 +70,12 @@ public:
     FORWARD_IUNKNOWN()
     
     AvnAppMenu(IAvnMenuEvents* events);
-        
+
     AvnMenu* GetNative();
     
     void RaiseNeedsUpdate ();
+    void RaiseOpening();
+    void RaiseClosed();
     
     virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;
     

+ 74 - 45
native/Avalonia.Native/src/OSX/menu.mm

@@ -71,12 +71,12 @@
 }
 @end
 
-AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
+AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
 {
     _isCheckable = false;
-    _isSeperator = isSeperator;
+    _isSeparator = isSeparator;
     
-    if(isSeperator)
+    if(isSeparator)
     {
         _native = [NSMenuItem separatorItem];
     }
@@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
     }
 }
 
+void AvnAppMenu::RaiseOpening()
+{
+    if(_baseEvents != nullptr)
+    {
+        _baseEvents->Opening();
+    }
+}
+
+void AvnAppMenu::RaiseClosed()
+{
+    if(_baseEvents != nullptr)
+    {
+        _baseEvents->Closed();
+    }
+}
+
+
 HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
 {
     @autoreleasepool
@@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
     _parent->RaiseNeedsUpdate();
 }
 
+- (void)menuWillOpen:(NSMenu *)menu
+{
+    _parent->RaiseOpening();
+}
+
+- (void)menuDidClose:(NSMenu *)menu
+{
+    _parent->RaiseClosed();
+}
 
 @end
 
@@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem()
     }
 }
 
-extern IAvnMenuItem* CreateAppMenuItemSeperator()
+extern IAvnMenuItem* CreateAppMenuItemSeparator()
 {
     @autoreleasepool
     {
@@ -445,47 +471,50 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
         
         auto appMenu  = [s_appMenuItem submenu];
         
-        [appMenu addItem:[NSMenuItem separatorItem]];
-        
-        // Services item and menu
-        auto servicesItem = [[NSMenuItem alloc] init];
-        servicesItem.title = @"Services";
-        NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"];
-        servicesItem.submenu = servicesMenu;
-        [NSApplication sharedApplication].servicesMenu = servicesMenu;
-        [appMenu addItem:servicesItem];
-        
-        [appMenu addItem:[NSMenuItem separatorItem]];
-        
-        // Hide Application
-        auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"];
-        
-        [appMenu addItem:hideItem];
-        
-        // Hide Others
-        auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
-                                                       action:@selector(hideOtherApplications:)
-                                                keyEquivalent:@"h"];
-        
-        hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
-        [appMenu addItem:hideAllOthersItem];
-        
-        // Show All
-        auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
-                                                 action:@selector(unhideAllApplications:)
-                                          keyEquivalent:@""];
-        
-        [appMenu addItem:showAllItem];
-        
-        [appMenu addItem:[NSMenuItem separatorItem]];
-        
-        // Quit Application
-        auto quitItem = [[NSMenuItem alloc] init];
-        quitItem.title = [@"Quit " stringByAppendingString:appName];
-        quitItem.keyEquivalent = @"q";
-        quitItem.target = [AvnWindow class];
-        quitItem.action = @selector(closeAll);
-        [appMenu addItem:quitItem];
+        if(GetAutoGenerateDefaultAppMenuItems())
+        {
+            [appMenu addItem:[NSMenuItem separatorItem]];
+            
+            // Services item and menu
+            auto servicesItem = [[NSMenuItem alloc] init];
+            servicesItem.title = @"Services";
+            NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"];
+            servicesItem.submenu = servicesMenu;
+            [NSApplication sharedApplication].servicesMenu = servicesMenu;
+            [appMenu addItem:servicesItem];
+            
+            [appMenu addItem:[NSMenuItem separatorItem]];
+            
+            // Hide Application
+            auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"];
+            
+            [appMenu addItem:hideItem];
+            
+            // Hide Others
+            auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
+                                                           action:@selector(hideOtherApplications:)
+                                                    keyEquivalent:@"h"];
+            
+            hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
+            [appMenu addItem:hideAllOthersItem];
+            
+            // Show All
+            auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
+                                                     action:@selector(unhideAllApplications:)
+                                              keyEquivalent:@""];
+            
+            [appMenu addItem:showAllItem];
+            
+            [appMenu addItem:[NSMenuItem separatorItem]];
+            
+            // Quit Application
+            auto quitItem = [[NSMenuItem alloc] init];
+            quitItem.title = [@"Quit " stringByAppendingString:appName];
+            quitItem.keyEquivalent = @"q";
+            quitItem.target = [AvnWindow class];
+            quitItem.action = @selector(closeAll);
+            [appMenu addItem:quitItem];
+        }
     }
     else
     {

+ 11 - 3
native/Avalonia.Native/src/OSX/window.mm

@@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
     for(int i = 0; i < numWindows; i++)
     {
-        [[windows objectAtIndex:i] performClose:nil];
+        auto window = (AvnWindow*)[windows objectAtIndex:i];
+        
+        if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
+        {
+            [window performClose:nil];
+        }
     }
 }
 
@@ -2226,9 +2231,12 @@ protected:
     {
         @autoreleasepool
         {
-            [Window setContentSize:NSSize{x, y}];
+            if (Window != nullptr)
+            {
+                [Window setContentSize:NSSize{x, y}];
             
-            [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
+                [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
+            }
             
             return S_OK;
         }

+ 0 - 4
nukebuild/Build.cs

@@ -89,10 +89,6 @@ partial class Build : NukeBuild
             Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
         }
         ExecWait("dotnet version:", "dotnet", "--version");
-        if (Parameters.IsRunningOnUnix)
-            ExecWait("Mono version:", "mono", "--version");
-
-
     }
 
     IReadOnlyCollection<Output> MsBuildCommon(

+ 1 - 1
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

+ 1 - 1
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
-	<uses-sdk android:targetSdkVersion="29" />
+	<uses-sdk android:targetSdkVersion="30" />
 	<application android:label="ControlCatalog.Android"></application>
 </manifest>

+ 2 - 2
samples/ControlCatalog/MainWindow.xaml

@@ -18,11 +18,11 @@
       <NativeMenuItem Header="File">
         <NativeMenu>
           <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
-          <NativeMenuItemSeperator/>
+          <NativeMenuItemSeperator/><!-- Uses incorrect spelling to demonstrate backwards compatibility -->
           <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
             <NativeMenu/>
           </NativeMenuItem>
-          <NativeMenuItemSeperator/>
+          <NativeMenuItemSeparator/>
           <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
                           Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
                           Clicked="OnCloseClicked" />

+ 0 - 47
src/Android/Avalonia.Android/ActivityTracker.cs

@@ -1,47 +0,0 @@
-using Android.App;
-using Android.OS;
-
-namespace Avalonia.Android
-{
-    internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
-    {
-        public static Activity Current { get; private set; }
-        public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityDestroyed(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-
-        public void OnActivityPaused(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-
-        public void OnActivityResumed(Activity activity)
-        {
-            Current = activity;
-        }
-
-        public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityStarted(Activity activity)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityStopped(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-    }
-}

+ 7 - 25
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -29,60 +29,42 @@ namespace Avalonia
 
 namespace Avalonia.Android
 {
-    class AndroidPlatform : IPlatformSettings, IWindowingPlatform
+    class AndroidPlatform : IPlatformSettings
     {
         public static readonly AndroidPlatform Instance = new AndroidPlatform();
+        public static AndroidPlatformOptions Options { get; private set; }
         public Size DoubleClickSize => new Size(4, 4);
         public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
-        public double RenderScalingFactor => _scalingFactor;
-        public double LayoutScalingFactor => _scalingFactor;
-
-        private readonly double _scalingFactor = 1;
-
-        public AndroidPlatform()
-        {
-            _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
-        }
 
         public static void Initialize(Type appType, AndroidPlatformOptions options)
         {
+            Options = options;
+
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToTransient<ClipboardImpl>()
-                .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
+                .Bind<ICursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
                 .Bind<IPlatformSettings>().ToConstant(Instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
                 .Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
-                .Bind<IWindowingPlatform>().ToConstant(Instance)
                 .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
-                .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
+                .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
                 .Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
 
             SkiaPlatform.Initialize();
-            ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
-                .RegisterActivityLifecycleCallbacks(new ActivityTracker());
 
             if (options.UseGpu)
             {
                 EglPlatformOpenGlInterface.TryInitialize();
             }
         }
-
-        public IWindowImpl CreateWindow()
-        {
-            throw new NotSupportedException();
-        }
-
-        public IWindowImpl CreateEmbeddableWindow()
-        {
-            throw new NotSupportedException();
-        }
     }
 
     public sealed class AndroidPlatformOptions
     {
+        public bool UseDeferredRendering { get; set; } = true;
         public bool UseGpu { get; set; } = true;
     }
 }

+ 8 - 7
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@@ -1,25 +1,26 @@
 using System;
 using System.Reactive.Disposables;
 using System.Threading;
+
 using Android.OS;
+
 using Avalonia.Platform;
 using Avalonia.Threading;
 
+using App = Android.App.Application;
+
 namespace Avalonia.Android
 {
-    class AndroidThreadingInterface : IPlatformThreadingInterface
+    internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
     {
         private Handler _handler;
 
         public AndroidThreadingInterface()
         {
-            _handler = new Handler(global::Android.App.Application.Context.MainLooper);
+            _handler = new Handler(App.Context.MainLooper);
         }
 
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            return;
-        }
+        public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
 
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
         {
@@ -57,7 +58,7 @@ namespace Avalonia.Android
                     });
                 }
             }, null, TimeSpan.Zero, interval);
-            
+
             return Disposable.Create(() =>
             {
                 lock (l)

+ 1 - 1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="MSBuild.Sdk.Extras">
   <PropertyGroup>
-    <TargetFramework>monoandroid90</TargetFramework>
+    <TargetFramework>monoandroid11.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>

+ 3 - 8
src/Android/Avalonia.Android/AvaloniaActivity.cs

@@ -1,4 +1,3 @@
-
 using Android.App;
 using Android.OS;
 using Android.Views;
@@ -7,15 +6,13 @@ namespace Avalonia.Android
 {
     public abstract class AvaloniaActivity : Activity
     {
-        
         internal AvaloniaView View;
         object _content;
 
         protected override void OnCreate(Bundle savedInstanceState)
         {
-            RequestWindowFeature(WindowFeatures.NoTitle);
             View = new AvaloniaView(this);
-            if(_content != null)
+            if (_content != null)
                 View.Content = _content;
             SetContentView(View);
             TakeKeyEvents(true);
@@ -36,9 +33,7 @@ namespace Avalonia.Android
             }
         }
 
-        public override bool DispatchKeyEvent(KeyEvent e)
-        {
-            return View.DispatchKeyEvent(e);
-        }
+        public override bool DispatchKeyEvent(KeyEvent e) =>
+            View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
     }
 }

+ 34 - 1
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -1,11 +1,12 @@
 using System;
 using Android.Content;
+using Android.Runtime;
 using Android.Views;
 using Android.Widget;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls;
 using Avalonia.Controls.Embedding;
-using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.Android
 {
@@ -14,6 +15,8 @@ namespace Avalonia.Android
         private readonly EmbeddableControlRoot _root;
         private readonly ViewImpl _view;
 
+        private IDisposable? _timerSubscription;
+
         public AvaloniaView(Context context) : base(context)
         {
             _view = new ViewImpl(context);
@@ -33,6 +36,36 @@ namespace Avalonia.Android
             return _view.View.DispatchKeyEvent(e);
         }
 
+        public override void OnVisibilityAggregated(bool isVisible)
+        {
+            base.OnVisibilityAggregated(isVisible);
+            OnVisibilityChanged(isVisible);
+        }
+
+        protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
+        {
+            base.OnVisibilityChanged(changedView, visibility);
+            OnVisibilityChanged(visibility == ViewStates.Visible);
+        }
+
+        private void OnVisibilityChanged(bool isVisible)
+        {
+            if (isVisible)
+            {
+                if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
+                {
+                    _timerSubscription = timer.SubscribeView(this);
+                }
+
+                _root.Renderer.Start();
+            }
+            else
+            {
+                _root.Renderer.Stop();
+                _timerSubscription?.Dispose();
+            }
+        }
+
         class ViewImpl : TopLevelImpl
         {
             public ViewImpl(Context context) : base(context)

+ 101 - 0
src/Android/Avalonia.Android/ChoreographerTimer.cs

@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+
+using Android.OS;
+using Android.Views;
+
+using Avalonia.Rendering;
+
+using Java.Lang;
+
+namespace Avalonia.Android
+{
+    internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
+    {
+        private readonly object _lock = new object();
+
+        private readonly Thread _thread;
+        private readonly TaskCompletionSource<Choreographer> _choreographer = new TaskCompletionSource<Choreographer>();
+
+        private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
+
+        private Action<TimeSpan> _tick;
+        private int _count;
+
+        public ChoreographerTimer()
+        {
+            _thread = new Thread(Loop);
+            _thread.Start();
+        }
+
+        public event Action<TimeSpan> Tick
+        {
+            add
+            {
+                lock (_lock)
+                {
+                    _tick += value;
+                    _count++;
+
+                    if (_count == 1)
+                    {
+                        _choreographer.Task.Result.PostFrameCallback(this);
+                    }
+                }
+            }
+            remove
+            {
+                lock (_lock)
+                {
+                    _tick -= value;
+                    _count--;
+                }
+            }
+        }
+
+        internal IDisposable SubscribeView(AvaloniaView view)
+        {
+            lock (_lock)
+            {
+                _views.Add(view);
+
+                if (_views.Count == 1)
+                {
+                    _choreographer.Task.Result.PostFrameCallback(this);
+                }
+            }
+
+            return Disposable.Create(
+                () =>
+                {
+                    lock (_lock)
+                    {
+                        _views.Remove(view);
+                    }
+                }
+            );
+        }
+
+        private void Loop()
+        {
+            Looper.Prepare();
+            _choreographer.SetResult(Choreographer.Instance);
+            Looper.Loop();
+        }
+
+        public void DoFrame(long frameTimeNanos)
+        {
+            _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
+
+            lock (_lock)
+            {
+                if (_count > 0 && _views.Count > 0)
+                {
+                    Choreographer.Instance.PostFrameCallback(this);
+                }
+            }
+        }
+    }
+}

+ 13 - 4
src/Android/Avalonia.Android/CursorFactory.cs

@@ -1,12 +1,21 @@
-using System;
 using Avalonia.Input;
 using Avalonia.Platform;
 
 namespace Avalonia.Android
 {
-    internal class CursorFactory : IStandardCursorFactory
+    internal class CursorFactory : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
-            => new PlatformHandle(IntPtr.Zero, "ZeroCursor");
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor;
+
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor;
+
+        private sealed class CursorImpl : ICursorImpl
+        {
+            public static CursorImpl ZeroCursor { get; } = new CursorImpl();
+
+            private CursorImpl() { }
+
+            public void Dispose() { }
+        }
     }
 }

+ 2 - 4
src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs

@@ -1,6 +1,4 @@
-using System.Linq;
-
-using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Egl;
 using Avalonia.OpenGL.Surfaces;
 
 namespace Avalonia.Android.OpenGL
@@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL
         }
 
         public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
-            new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle));
+            new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
 
         public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
         {

+ 10 - 3
src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs

@@ -1,23 +1,30 @@
-using Avalonia.OpenGL.Egl;
+using System;
+
+using Avalonia.OpenGL.Egl;
 using Avalonia.OpenGL.Surfaces;
 
 namespace Avalonia.Android.OpenGL
 {
-    internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase
+    internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
     {
         private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
         private readonly EglSurface _surface;
+        private readonly IntPtr _handle;
 
         public GlRenderTarget(
             EglPlatformOpenGlInterface egl,
             EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
-            EglSurface surface)
+            EglSurface surface,
+            IntPtr handle)
             : base(egl)
         {
             _info = info;
             _surface = surface;
+            _handle = handle;
         }
 
+        public bool IsCorrupted => _handle != _info.Handle;
+
         public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
     }
 }

+ 0 - 14
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@@ -1,14 +0,0 @@
-using Avalonia.Input;
-
-namespace Avalonia.Android.Platform.Input
-{
-    public class AndroidMouseDevice : MouseDevice
-    {
-        public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice();
-
-        public AndroidMouseDevice()
-        {
-
-        }
-    }
-}

+ 4 - 2
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
     {
         private IntPtr _window;
 
-        public AndroidFramebuffer(Surface surface)
+        public AndroidFramebuffer(Surface surface, double scaling)
         {
             if(surface == null)
                 throw new ArgumentNullException(nameof(surface));
@@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
             Address = buffer.bits;
+
+            Dpi = scaling * new Vector(96, 96);
         }
 
         public void Dispose()
@@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public IntPtr Address { get; set; }
         public PixelSize Size { get; }
         public int RowBytes { get; }
-        public Vector Dpi { get; } = new Vector(96, 96);
+        public Vector Dpi { get; }
         public PixelFormat Format { get; }
 
         [DllImport("android")]

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs

@@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _topLevel = topLevel;
         }
 
-        public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface);
+        public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
     }
 }

+ 2 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@@ -43,11 +43,13 @@ namespace Avalonia.Android
             }
         }
 
+        [Obsolete("deprecated")]
         public override void Invalidate(global::Android.Graphics.Rect dirty)
         {
             Invalidate();
         }
 
+        [Obsolete("deprecated")]
         public override void Invalidate(int l, int t, int r, int b)
         {
             Invalidate();

+ 18 - 30
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -6,7 +6,6 @@ using Android.Runtime;
 using Android.Views;
 
 using Avalonia.Android.OpenGL;
-using Avalonia.Android.Platform.Input;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific.Helpers;
 using Avalonia.Controls;
@@ -35,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _view = new ViewImpl(context, this, placeOnTop);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
-                p => GetAvaloniaPointFromEvent(p));
+                GetAvaloniaPointFromEvent);
 
             _gl = GlPlatformSurface.TryCreate(this);
             _framebuffer = new FramebufferManager(this);
 
-            MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
-                _view.Resources.DisplayMetrics.HeightPixels);
-        }
-
+            RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
 
+            MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
+                _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
+        }
 
         private bool _handleEvents;
 
@@ -58,25 +57,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
 
-        public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY());
+        public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
+            new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
 
         public IInputRoot InputRoot { get; private set; }
 
-        public virtual Size ClientSize
-        {
-            get
-            {
-                if (_view == null)
-                    return new Size(0, 0);
-                return new Size(_view.Width, _view.Height);
-            }
-            set
-            {
-
-            }
-        }
+        public virtual Size ClientSize => Size.ToSize(RenderScaling);
 
-        public IMouseDevice MouseDevice => AndroidMouseDevice.Instance;
+        public IMouseDevice MouseDevice { get; } = new MouseDevice();
 
         public Action Closed { get; set; }
 
@@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer };
 
-        public IRenderer CreateRenderer(IRenderRoot root)
-        {
-            return new ImmediateRenderer(root);
-        }
+        public IRenderer CreateRenderer(IRenderRoot root) =>
+            AndroidPlatform.Options.UseDeferredRendering
+            ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
+            : new ImmediateRenderer(root);
 
         public virtual void Hide()
         {
@@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public Point PointToClient(PixelPoint point)
         {
-            return point.ToPoint(1);
+            return point.ToPoint(RenderScaling);
         }
 
         public PixelPoint PointToScreen(Point point)
         {
-            return PixelPoint.FromPoint(point, 1);
+            return PixelPoint.FromPoint(point, RenderScaling);
         }
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
             //still not implemented
         }
@@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _view.Visibility = ViewStates.Visible;
         }
 
-        public double RenderScaling => 1;
+        public double RenderScaling { get; }
 
         void Draw()
         {
@@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
             {
-                var newSize = new Size(width, height);
+                var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
 
                 if (newSize != _oldSize)
                 {

+ 22 - 69
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
         private TView _view;
         public bool HandleEvents { get; set; }
 
-        public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, Point> getPointfunc)
+        public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, int, Point> getPointfunc)
         {
             this._view = view;
             HandleEvents = true;
@@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers
             _getInputRoot = getInputRoot;
         }
 
-        private DateTime _lastTouchMoveEventTime = DateTime.Now;
-        private Point? _lastTouchMovePoint;
-        private Func<MotionEvent, Point> _getPointFunc;
+        private TouchDevice _touchDevice = new TouchDevice();
+        private Func<MotionEvent, int, Point> _getPointFunc;
         private Func<IInputRoot> _getInputRoot;
-        private Point _point;
 
         public bool? DispatchTouchEvent(MotionEvent e, out bool callBase)
         {
@@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                 return null;
             }
 
-            RawPointerEventType? mouseEventType = null;
             var eventTime = DateTime.Now;
+
             //Basic touch support
-            switch (e.Action)
+            var pointerEventType = e.Action switch
             {
-                case MotionEventActions.Move:
-                    //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
-                    if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
-                    {
-                        mouseEventType = RawPointerEventType.Move;
-                    }
-                    break;
-
-                case MotionEventActions.Down:
-                    mouseEventType = RawPointerEventType.LeftButtonDown;
+                MotionEventActions.Down => RawPointerEventType.TouchBegin,
+                MotionEventActions.Up => RawPointerEventType.TouchEnd,
+                MotionEventActions.Cancel => RawPointerEventType.TouchCancel,
+                _ => RawPointerEventType.TouchUpdate
+            };
 
-                    break;
+            if (e.Action.HasFlag(MotionEventActions.PointerDown))
+            {
+                pointerEventType = RawPointerEventType.TouchBegin;
+            }
 
-                case MotionEventActions.Up:
-                    mouseEventType = RawPointerEventType.LeftButtonUp;
-                    break;
+            if (e.Action.HasFlag(MotionEventActions.PointerUp))
+            {
+                pointerEventType = RawPointerEventType.TouchEnd;
             }
 
-            if (mouseEventType != null)
+            for (int i = 0; i < e.PointerCount; i++)
             {
                 //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event
-                _point = _getPointFunc(e);
+                var point = _getPointFunc(e, i);
 
                 double x = _view.View.GetX();
                 double y = _view.View.GetY();
                 double r = x + _view.View.Width;
                 double b = y + _view.View.Height;
 
-                if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y)
+                if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y)
                 {
                     var inputRoot = _getInputRoot();
-                    var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
-
-                    //in order the controls to work in a predictable way
-                    //we need to generate mouse move before first mouse down event
-                    //as this is the way buttons are working every time
-                    //otherwise there is a problem sometimes
-                    if (mouseEventType == RawPointerEventType.LeftButtonDown)
-                    {
-                        var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
-                                    RawPointerEventType.Move, _point, RawInputModifiers.None);
-                        _view.Input(me);
-                    }
 
-                    var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
-                        mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton);
+                    var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot,
+                        i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i));
                     _view.Input(mouseEvent);
-
-                    if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null)
-                    {
-                        if (_lastTouchMovePoint != null)
-                        {
-                            //raise mouse scroll event so the scrollers
-                            //are moving with the cursor
-                            double vectorX = _point.X - _lastTouchMovePoint.Value.X;
-                            double vectorY = _point.Y - _lastTouchMovePoint.Value.Y;
-                            //based on test correction of 0.02 is working perfect
-                            double correction = 0.02;
-                            var ps = AndroidPlatform.Instance.LayoutScalingFactor;
-                            var mouseWheelEvent = new RawMouseWheelEventArgs(
-                                        mouseDevice,
-                                        (uint)eventTime.Ticks,
-                                        inputRoot,
-                                        _point,
-                                        new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton);
-                            _view.Input(mouseWheelEvent);
-                        }
-                        _lastTouchMovePoint = _point;
-                        _lastTouchMoveEventTime = eventTime;
-                    }
-                    else if (e.Action == MotionEventActions.Down)
-                    {
-                        _lastTouchMovePoint = _point;
-                    }
-                    else
-                    {
-                        _lastTouchMovePoint = null;
-                    }
                 }
             }
 

+ 0 - 11
src/Android/Avalonia.Android/app.config

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
-  <runtime>
-    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
-      <dependentAssembly>
-        <assemblyIdentity name="System.Runtime.InteropServices.WindowsRuntime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
-        <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
-      </dependentAssembly>
-    </assemblyBinding>
-  </runtime>
-</configuration>

+ 2 - 2
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -150,4 +150,4 @@
   <Import Project="..\..\..\build\System.Memory.props" />
   <Import Project="..\..\..\build\AndroidWorkarounds.props" />
   <Import Project="..\..\..\build\LegacyProject.targets" />
-</Project>
+</Project>

+ 1 - 1
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
-	<uses-sdk android:targetSdkVersion="29" />
+	<uses-sdk android:targetSdkVersion="30" />
 	<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
 	<uses-permission android:name="android.permission.INTERNET" />
 </manifest>

+ 24 - 4
src/Avalonia.Animation/Animatable.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Linq;
 using Avalonia.Data;
 
 #nullable enable
@@ -93,16 +94,35 @@ namespace Avalonia.Animation
                 var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
                 var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
 
+                // When transitions are replaced, we add the new transitions before removing the old
+                // transitions, so that when the old transition being disposed causes the value to
+                // change, there is a corresponding entry in `_transitionStates`. This means that we
+                // need to account for any transitions present in both the old and new transitions
+                // collections.
                 if (newTransitions is object)
                 {
+                    var toAdd = (IList)newTransitions;
+
+                    if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
+                    {
+                        toAdd = newTransitions.Except(oldTransitions).ToList();
+                    }
+
                     newTransitions.CollectionChanged += TransitionsCollectionChanged;
-                    AddTransitions(newTransitions);
+                    AddTransitions(toAdd);
                 }
 
                 if (oldTransitions is object)
                 {
+                    var toRemove = (IList)oldTransitions;
+
+                    if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
+                    {
+                        toRemove = oldTransitions.Except(newTransitions).ToList();
+                    }
+
                     oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
-                    RemoveTransitions(oldTransitions);
+                    RemoveTransitions(toRemove);
                 }
             }
             else if (_transitionsEnabled &&
@@ -115,9 +135,9 @@ namespace Avalonia.Animation
                 {
                     var transition = Transitions[i];
 
-                    if (transition.Property == change.Property)
+                    if (transition.Property == change.Property &&
+                        _transitionState.TryGetValue(transition, out var state))
                     {
-                        var state = _transitionState[transition];
                         var oldValue = state.BaseValue;
                         var newValue = GetAnimationBaseValue(transition.Property);
 

+ 8 - 8
src/Avalonia.Animation/Animation.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, TimeSpan> DurationProperty =
             AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
-                nameof(_duration),
+                nameof(Duration),
                 o => o._duration,
                 (o, v) => o._duration = v);
 
@@ -31,7 +31,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, IterationCount> IterationCountProperty =
             AvaloniaProperty.RegisterDirect<Animation, IterationCount>(
-                nameof(_iterationCount),
+                nameof(IterationCount),
                 o => o._iterationCount,
                 (o, v) => o._iterationCount = v);
 
@@ -40,7 +40,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, PlaybackDirection> PlaybackDirectionProperty =
             AvaloniaProperty.RegisterDirect<Animation, PlaybackDirection>(
-                nameof(_playbackDirection),
+                nameof(PlaybackDirection),
                 o => o._playbackDirection,
                 (o, v) => o._playbackDirection = v);
 
@@ -49,7 +49,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, FillMode> FillModeProperty =
             AvaloniaProperty.RegisterDirect<Animation, FillMode>(
-                nameof(_fillMode),
+                nameof(FillMode),
                 o => o._fillMode,
                 (o, v) => o._fillMode = v);
 
@@ -58,7 +58,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, Easing> EasingProperty =
             AvaloniaProperty.RegisterDirect<Animation, Easing>(
-                nameof(_easing),
+                nameof(Easing),
                 o => o._easing,
                 (o, v) => o._easing = v);
 
@@ -67,7 +67,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, TimeSpan> DelayProperty =
             AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
-                nameof(_delay),
+                nameof(Delay),
                 o => o._delay,
                 (o, v) => o._delay = v);
 
@@ -76,7 +76,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, TimeSpan> DelayBetweenIterationsProperty =
             AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
-                nameof(_delayBetweenIterations),
+                nameof(DelayBetweenIterations),
                 o => o._delayBetweenIterations,
                 (o, v) => o._delayBetweenIterations = v);
 
@@ -85,7 +85,7 @@ namespace Avalonia.Animation
         /// </summary>
         public static readonly DirectProperty<Animation, double> SpeedRatioProperty =
             AvaloniaProperty.RegisterDirect<Animation, double>(
-                nameof(_speedRatio),
+                nameof(SpeedRatio),
                 o => o._speedRatio,
                 (o, v) => o._speedRatio = v,
                 defaultBindingMode: BindingMode.TwoWay);

+ 39 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -23,7 +23,7 @@ namespace Avalonia
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
         private List<IAvaloniaObject> _inheritanceChildren;
         private ValueStore _values;
-        private ValueStore Values => _values ?? (_values = new ValueStore(this));
+        private bool _batchUpdate;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@@ -117,6 +117,22 @@ namespace Avalonia
             set { this.Bind(binding.Property, value); }
         }
 
+        private ValueStore Values
+        {
+            get
+            {
+                if (_values is null)
+                {
+                    _values = new ValueStore(this);
+
+                    if (_batchUpdate)
+                        _values.BeginBatchUpdate();
+                }
+
+                return _values;
+            }
+        }
+
         public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
 
         public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -434,6 +450,28 @@ namespace Avalonia
             _values?.CoerceValue(property);
         }
 
+        public void BeginBatchUpdate()
+        {
+            if (_batchUpdate)
+            {
+                throw new InvalidOperationException("Batch update already in progress.");
+            }
+
+            _batchUpdate = true;
+            _values?.BeginBatchUpdate();
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (!_batchUpdate)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            _batchUpdate = false;
+            _values?.EndBatchUpdate();
+        }
+
         /// <inheritdoc/>
         void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
         {

+ 6 - 0
src/Avalonia.Base/Data/Converters/BoolConverters.cs

@@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters
         /// </summary>
         public static readonly IMultiValueConverter And =
             new FuncMultiValueConverter<bool, bool>(x => x.All(y => y));
+
+        /// <summary>
+        /// A multi-value converter that returns true if any of the inputs is true.
+        /// </summary>
+        public static readonly IMultiValueConverter Or =
+            new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y));
     }
 }

+ 53 - 7
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
     /// <summary>
     /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
     /// </summary>
-    internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+    internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
     {
+        void Start(bool ignoreBatchUpdate);
     }
 
     /// <summary>
@@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
         private readonly IAvaloniaObject _owner;
         private IValueSink _sink;
         private IDisposable? _subscription;
+        private bool _isSubscribed;
+        private bool _batchUpdate;
         private Optional<T> _value;
 
         public BindingEntry(
@@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         public IObservable<BindingValue<T>> Source { get; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate() => _batchUpdate = true;
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            if (_sink is ValueStore)
+                Start();
+        }
+
         public Optional<T> GetValue(BindingPriority maxPriority)
         {
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
@@ -52,10 +65,17 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _sink.Completed(Property, this, _value);
+            OnCompleted();
         }
 
-        public void OnCompleted() => _sink.Completed(Property, this, _value);
+        public void OnCompleted()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _isSubscribed = false;
+            _sink.Completed(Property, this, oldValue);
+        }
 
         public void OnError(Exception error)
         {
@@ -79,13 +99,39 @@ namespace Avalonia.PropertyStore
             }
         }
 
-        public void Start()
+        public void Start() => Start(false);
+
+        public void Start(bool ignoreBatchUpdate)
         {
-            _subscription = Source.Subscribe(this);
+            // We can't use _subscription to check whether we're subscribed because it won't be set
+            // until Subscribe has finished, which will be too late to prevent reentrancy. In addition
+            // don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
+            if (!_isSubscribed &&
+                Priority != BindingPriority.Unset &&
+                (!_batchUpdate || ignoreBatchUpdate))
+            {
+                _isSubscribed = true;
+                _subscription = Source.Subscribe(this);
+            }
         }
 
         public void Reparent(IValueSink sink) => _sink = sink;
-        
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
+
         private void UpdateValue(BindingValue<T> value)
         {
             if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

+ 35 - 4
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -1,23 +1,31 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 
 #nullable enable
 
 namespace Avalonia.PropertyStore
 {
+    /// <summary>
+    /// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
+    /// </summary>
+    internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+    {
+    }
+
     /// <summary>
     /// Stores a value with a priority in a <see cref="ValueStore"/> or
     /// <see cref="PriorityValue{T}"/>.
     /// </summary>
     /// <typeparam name="T">The property type.</typeparam>
-    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
+    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
     {
         private IValueSink _sink;
         private Optional<T> _value;
 
         public ConstantValueEntry(
             StyledPropertyBase<T> property,
-            T value,
+            [AllowNull] T value,
             BindingPriority priority,
             IValueSink sink)
         {
@@ -28,7 +36,7 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
         public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@@ -36,7 +44,30 @@ namespace Avalonia.PropertyStore
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
         }
 
-        public void Dispose() => _sink.Completed(Property, this, _value);
+        public void Dispose()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _sink.Completed(Property, this, oldValue);
+        }
+
         public void Reparent(IValueSink sink) => _sink = sink;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
     }
 }

+ 8 - 0
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@@ -0,0 +1,8 @@
+namespace Avalonia.PropertyStore
+{
+    internal interface IBatchUpdate
+    {
+        void BeginBatchUpdate();
+        void EndBatchUpdate();
+    }
+}

+ 8 - 1
src/Avalonia.Base/PropertyStore/IValue.cs

@@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
     /// </summary>
     internal interface IValue
     {
-        Optional<object> GetValue();
         BindingPriority Priority { get; }
+        Optional<object> GetValue();
+        void Start();
+        void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue);
     }
 
     /// <summary>

+ 16 - 0
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore
         }
 
         public void SetValue(T value) => _value = value;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                BindingPriority.LocalValue));
+        }
     }
 }

+ 95 - 26
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
     /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
     /// they were added) plus a local value.
     /// </remarks>
-    internal class PriorityValue<T> : IValue<T>, IValueSink
+    internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
     {
         private readonly IAvaloniaObject _owner;
         private readonly IValueSink _sink;
@@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
         private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
         private Optional<T> _localValue;
         private Optional<T> _value;
+        private bool _isCalculatingValue;
+        private bool _batchUpdate;
 
         public PriorityValue(
             IAvaloniaObject owner,
@@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
             existing.Reparent(this);
             _entries.Add(existing);
 
+            if (existing is IBindingEntry binding &&
+                existing.Priority == BindingPriority.LocalValue)
+            {
+                // Bit of a special case here: if we have a local value binding that is being
+                // promoted to a priority value we need to make sure the binding is subscribed
+                // even if we've got a batch operation in progress because otherwise we don't know
+                // whether the binding or a subsequent SetValue with local priority will win. A
+                // notification won't be sent during batch update anyway because it will be
+                // caught and stored for later by the ValueStore.
+                binding.Start(ignoreBatchUpdate: true);
+            }
+
             var v = existing.GetValue();
             
             if (v.HasValue)
@@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
         public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate()
+        {
+            _batchUpdate = true;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.BeginBatchUpdate();
+            }
+        }
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.EndBatchUpdate();
+            }
+
+            UpdateEffectiveValue(null);
+        }
+
         public void ClearLocalValue()
         {
             UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
@@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
             var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
             var insert = FindInsertPoint(binding.Priority);
             _entries.Insert(insert, binding);
+
+            if (_batchUpdate)
+            {
+                binding.BeginBatchUpdate();
+                
+                if (priority == BindingPriority.LocalValue)
+                {
+                    binding.Start(ignoreBatchUpdate: true);
+                }
+            }
+
             return binding;
         }
 
-        public void CoerceValue() => UpdateEffectiveValue(null);
+        public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
+        public void Start() => UpdateEffectiveValue(null);
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
 
         void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
         {
@@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
                 _localValue = default;
             }
 
-            if (change is AvaloniaPropertyChangedEventArgs<T> c)
+            if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
             {
                 UpdateEffectiveValue(c);
             }
@@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore
 
         public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
         {
-            var reachedLocalValues = false;
+            _isCalculatingValue = true;
 
-            for (var i = _entries.Count - 1; i >= 0; --i)
+            try
             {
-                var entry = _entries[i];
-
-                if (entry.Priority < maxPriority)
+                for (var i = _entries.Count - 1; i >= 0; --i)
                 {
-                    continue;
+                    var entry = _entries[i];
+
+                    if (entry.Priority < maxPriority)
+                    {
+                        continue;
+                    }
+
+                    entry.Start();
+
+                    if (entry.Priority >= BindingPriority.LocalValue &&
+                        maxPriority <= BindingPriority.LocalValue &&
+                        _localValue.HasValue)
+                    {
+                        return (_localValue, BindingPriority.LocalValue);
+                    }
+
+                    var entryValue = entry.GetValue();
+
+                    if (entryValue.HasValue)
+                    {
+                        return (entryValue, entry.Priority);
+                    }
                 }
 
-                if (!reachedLocalValues &&
-                    entry.Priority >= BindingPriority.LocalValue &&
-                    maxPriority <= BindingPriority.LocalValue &&
-                    _localValue.HasValue)
+                if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
                 {
                     return (_localValue, BindingPriority.LocalValue);
                 }
 
-                var entryValue = entry.GetValue();
-
-                if (entryValue.HasValue)
-                {
-                    return (entryValue, entry.Priority);
-                }
+                return (default, BindingPriority.Unset);
             }
-
-            if (!reachedLocalValues &&
-                maxPriority <= BindingPriority.LocalValue &&
-                _localValue.HasValue)
+            finally
             {
-                return (_localValue, BindingPriority.LocalValue);
+                _isCalculatingValue = false;
             }
-
-            return (default, BindingPriority.Unset);
         }
 
         private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)

+ 5 - 14
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 
@@ -22,6 +22,9 @@ namespace Avalonia.Utilities
             _entries = s_emptyEntries;
         }
 
+        public int Count => _entries.Length - 1;
+        public TValue this[int index] => _entries[index].Value;
+
         private (int, bool) TryFindEntry(int propertyId)
         {
             if (_entries.Length <= 12)
@@ -91,7 +94,7 @@ namespace Avalonia.Utilities
             return (0, false);
         }
 
-        public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
+        public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
         {
             (int index, bool found) = TryFindEntry(property.Id);
             if (!found)
@@ -163,18 +166,6 @@ namespace Avalonia.Utilities
             }
         }
 
-        public Dictionary<AvaloniaProperty, TValue> ToDictionary()
-        {
-            var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
-
-            for (int i = 0; i < _entries.Length - 1; ++i)
-            {
-                dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
-            }
-
-            return dict;
-        }
-
         private struct Entry
         {
             internal int PropertyId;

+ 259 - 42
src/Avalonia.Base/ValueStore.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
 using Avalonia.Utilities;
@@ -26,6 +28,7 @@ namespace Avalonia
         private readonly AvaloniaObject _owner;
         private readonly IValueSink _sink;
         private readonly AvaloniaPropertyValueStore<IValue> _values;
+        private BatchUpdate? _batchUpdate;
 
         public ValueStore(AvaloniaObject owner)
         {
@@ -33,9 +36,28 @@ namespace Avalonia
             _values = new AvaloniaPropertyValueStore<IValue>();
         }
 
+        public void BeginBatchUpdate()
+        {
+            _batchUpdate ??= new BatchUpdate(this);
+            _batchUpdate.Begin();
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (_batchUpdate is null)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            if (_batchUpdate.End())
+            {
+                _batchUpdate = null;
+            }
+        }
+
         public bool IsAnimating(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.Priority < BindingPriority.LocalValue;
             }
@@ -45,7 +67,7 @@ namespace Avalonia
 
         public bool IsSet(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.GetValue().HasValue;
             }
@@ -58,7 +80,7 @@ namespace Avalonia
             BindingPriority maxPriority,
             out T value)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var v = ((IValue<T>)slot).GetValue(maxPriority);
 
@@ -82,7 +104,7 @@ namespace Avalonia
 
             IDisposable? result = null;
 
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 result = SetExisting(slot, property, value, priority);
             }
@@ -90,23 +112,21 @@ namespace Avalonia
             {
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
-                _values.AddValue(property, entry);
+                AddValue(property, entry);
                 result = entry.SetValue(value, priority);
             }
             else
             {
-                var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
-
                 if (priority == BindingPriority.LocalValue)
                 {
-                    _values.AddValue(property, new LocalValueEntry<T>(value));
-                    _sink.ValueChanged(change);
+                    AddValue(property, new LocalValueEntry<T>(value));
+                    NotifyValueChanged<T>(property, default, value, priority);
                 }
                 else
                 {
                     var entry = new ConstantValueEntry<T>(property, value, priority, this);
-                    _values.AddValue(property, entry);
-                    _sink.ValueChanged(change);
+                    AddValue(property, entry);
+                    NotifyValueChanged<T>(property, default, value, priority);
                     result = entry;
                 }
             }
@@ -119,7 +139,7 @@ namespace Avalonia
             IObservable<BindingValue<T>> source,
             BindingPriority priority)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return BindExisting(slot, property, source, priority);
             }
@@ -128,62 +148,69 @@ namespace Avalonia
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
                 var binding = entry.AddBinding(source, priority);
-                _values.AddValue(property, entry);
-                binding.Start();
+                AddValue(property, entry);
                 return binding;
             }
             else
             {
                 var entry = new BindingEntry<T>(_owner, property, source, priority, this);
-                _values.AddValue(property, entry);
-                entry.Start();
+                AddValue(property, entry);
                 return entry;
             }
         }
 
         public void ClearLocalValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
                     p.ClearLocalValue();
                 }
-                else
+                else if (slot.Priority == BindingPriority.LocalValue)
                 {
-                    var remove = slot is ConstantValueEntry<T> c ?
-                        c.Priority == BindingPriority.LocalValue : 
-                        !(slot is IPriorityValueEntry<T>);
+                    var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
 
-                    if (remove)
+                    // During batch update values can't be removed immediately because they're needed to raise
+                    // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
+                    // by setting their priority to Unset.
+                    if (!IsBatchUpdating())
                     {
-                        var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
                         _values.Remove(property);
-                        _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                            _owner,
-                            property,
-                            new Optional<T>(old),
-                            default,
-                            BindingPriority.Unset));
                     }
+                    else if (slot is IDisposable d)
+                    {
+                        d.Dispose();
+                    }
+                    else
+                    {
+                        // Local value entries are optimized and contain only a single value field to save space,
+                        // so there's no way to mark them for removal at the end of a batch update. Instead convert
+                        // them to a constant value entry with Unset priority in the event of a local value being
+                        // cleared during a batch update.
+                        var sentinel = new ConstantValueEntry<T>(property, default, BindingPriority.Unset, _sink);
+                        _values.SetValue(property, sentinel);
+                    }
+
+                    NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
                 }
             }
         }
 
         public void CoerceValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
-                    p.CoerceValue();
+                    p.UpdateEffectiveValue();
                 }
             }
         }
 
         public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var slotValue = slot.GetValue();
                 return new Diagnostics.AvaloniaPropertyValue(
@@ -198,7 +225,17 @@ namespace Avalonia
 
         void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
         {
-            _sink.ValueChanged(change);
+            if (_batchUpdate is object)
+            {
+                if (change.IsEffectiveValueChange)
+                {
+                    NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
+                }
+            }
+            else
+            {
+                _sink.ValueChanged(change);
+            }
         }
 
         void IValueSink.Completed<T>(
@@ -206,13 +243,18 @@ namespace Avalonia
             IPriorityValueEntry entry,
             Optional<T> oldValue)
         {
-            if (_values.TryGetValue(property, out var slot))
+            // We need to include remove sentinels here so call `_values.TryGetValue` directly.
+            if (_values.TryGetValue(property, out var slot) && slot == entry)
             {
-                if (slot == entry)
+                if (_batchUpdate is null)
                 {
                     _values.Remove(property);
                     _sink.Completed(property, entry, oldValue);
                 }
+                else
+                {
+                    _batchUpdate.ValueChanged(property, oldValue.ToObject());
+                }
             }
         }
 
@@ -240,16 +282,13 @@ namespace Avalonia
                 {
                     var old = l.GetValue(BindingPriority.LocalValue);
                     l.SetValue(value);
-                    _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                        _owner,
-                        property,
-                        old,
-                        value,
-                        priority));
+                    NotifyValueChanged<T>(property, old, value, priority);
                 }
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
+                    if (IsBatchUpdating())
+                        priorityValue.BeginBatchUpdate();
                     result = priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
                 }
@@ -273,6 +312,11 @@ namespace Avalonia
             if (slot is IPriorityValueEntry<T> e)
             {
                 priorityValue = new PriorityValue<T>(_owner, property, this, e);
+
+                if (IsBatchUpdating())
+                {
+                    priorityValue.BeginBatchUpdate();
+                }
             }
             else if (slot is PriorityValue<T> p)
             {
@@ -289,8 +333,181 @@ namespace Avalonia
 
             var binding = priorityValue.AddBinding(source, priority);
             _values.SetValue(property, priorityValue);
-            binding.Start();
+            priorityValue.UpdateEffectiveValue();
             return binding;
         }
+
+        private void AddValue(AvaloniaProperty property, IValue value)
+        {
+            _values.AddValue(property, value);
+            if (IsBatchUpdating() && value is IBatchUpdate batch)
+                batch.BeginBatchUpdate();
+            value.Start();
+        }
+
+        private void NotifyValueChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            if (_batchUpdate is null)
+            {
+                _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                    _owner,
+                    property,
+                    oldValue,
+                    newValue,
+                    priority));
+            }
+            else
+            {
+                _batchUpdate.ValueChanged(property, oldValue.ToObject());
+            }
+        }
+
+        private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
+
+        private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
+        {
+            return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
+        }
+
+        private static bool IsRemoveSentinel(IValue value)
+        {
+            // Local value entries are optimized and contain only a single value field to save space,
+            // so there's no way to mark them for removal at the end of a batch update. Instead a
+            // ConstantValueEntry with a priority of Unset is used as a sentinel value.
+            return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
+        }
+
+        private class BatchUpdate
+        {
+            private ValueStore _owner;
+            private List<Notification>? _notifications;
+            private int _batchUpdateCount;
+            private int _iterator = -1;
+
+            public BatchUpdate(ValueStore owner) => _owner = owner;
+
+            public bool IsBatchUpdating => _batchUpdateCount > 0;
+
+            public void Begin()
+            {
+                if (_batchUpdateCount++ == 0)
+                {
+                    var values = _owner._values;
+
+                    for (var i = 0; i < values.Count; ++i)
+                    {
+                        (values[i] as IBatchUpdate)?.BeginBatchUpdate();
+                    }
+                }
+            }
+
+            public bool End()
+            {
+                if (--_batchUpdateCount > 0)
+                    return false;
+
+                var values = _owner._values;
+
+                // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
+                // but notifications will still not be raised because the owner ValueStore will still have a reference
+                // to this batch update object.
+                for (var i = 0; i < values.Count; ++i)
+                {
+                    (values[i] as IBatchUpdate)?.EndBatchUpdate();
+
+                    // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
+                    // does, abort and continue batch updating.
+                    if (_batchUpdateCount > 0)
+                        return false;
+                }
+
+                if (_notifications is object)
+                {
+                    // Raise all batched notifications. Doing this can cause other notifications to be added and even
+                    // cause a new batch update to start, so we need to handle _notifications being modified by storing
+                    // the index in field.
+                    _iterator = 0;
+
+                    for (; _iterator < _notifications.Count; ++_iterator)
+                    {
+                        var entry = _notifications[_iterator];
+
+                        if (values.TryGetValue(entry.property, out var slot))
+                        {
+                            var oldValue = entry.oldValue;
+                            var newValue = slot.GetValue();
+
+                            // Raising this notification can cause a new batch update to be started, which in turn
+                            // results in another change to the property. In this case we need to update the old value
+                            // so that the *next* notification has an oldValue which follows on from the newValue
+                            // raised here.
+                            _notifications[_iterator] = new Notification
+                            {
+                                property = entry.property,
+                                oldValue = newValue,
+                            };
+
+                            // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
+                            slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
+
+                            // During batch update values can't be removed immediately because they're needed to raise
+                            // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
+                            // their priority to Unset. We need to re-read the slot here because raising ValueChanged
+                            // could have caused it to be updated.
+                            if (values.TryGetValue(entry.property, out var updatedSlot) &&
+                                updatedSlot.Priority == BindingPriority.Unset)
+                            {
+                                values.Remove(entry.property);
+                            }
+                        }
+                        else
+                        {
+                            throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
+                        }
+
+                        // If a new batch update was started while ending this one, abort.
+                        if (_batchUpdateCount > 0)
+                            return false;
+                    }
+                }
+
+                _iterator = int.MaxValue - 1;
+                return true;
+            }
+
+            public void ValueChanged(AvaloniaProperty property, Optional<object> oldValue)
+            {
+                _notifications ??= new List<Notification>();
+
+                for (var i = 0; i < _notifications.Count; ++i)
+                {
+                    if (_notifications[i].property == property)
+                    {
+                        oldValue = _notifications[i].oldValue;
+                        _notifications.RemoveAt(i);
+
+                        if (i <= _iterator)
+                            --_iterator;
+                        break;
+                    }
+                }
+
+                _notifications.Add(new Notification
+                {
+                    property = property,
+                    oldValue = oldValue,
+                });
+            }
+
+            private struct Notification
+            {
+                public AvaloniaProperty property;
+                public Optional<object> oldValue;
+            }
+        }
     }
 }

+ 12 - 0
src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs

@@ -0,0 +1,12 @@
+using System;
+using XamlX.Transform;
+
+namespace Avalonia.Build.Tasks
+{
+    public class DeterministicIdGenerator : IXamlIdentifierGenerator
+    {
+        private int _nextId = 1;
+        
+        public string GenerateIdentifierPart() => (_nextId++).ToString();
+    }
+}

+ 2 - 2
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@@ -22,7 +22,6 @@ using XamlX.IL;
 
 namespace Avalonia.Build.Tasks
 {
-    
     public static partial class XamlCompilerTaskExecutor
     {
         static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks
                 XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
                 AvaloniaXamlIlLanguage.CustomValueConverter,
                 new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
-                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)));
+                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
+                new DeterministicIdGenerator());
 
 
             var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", 

+ 9 - 2
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -3275,7 +3275,7 @@ namespace Avalonia.Collections
                 addIndex);
 
             // next check if we need to add an item into the current group
-            // bool needsGrouping = false;
+            bool needsGrouping = false;
             if (Count == 1 && GroupDescriptions.Count > 0)
             {
                 // if this is the first item being added
@@ -3302,7 +3302,7 @@ namespace Avalonia.Collections
                 // otherwise, we need to validate that it is within the current page.
                 if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
                 {
-                    //needsGrouping = true;
+                    needsGrouping = true;
 
                     int pageStartIndex = PageIndex * PageSize;
 
@@ -3340,6 +3340,13 @@ namespace Avalonia.Collections
                 }
             }
 
+            // if we need to add the item into the current group
+            // that will be displayed
+            if (needsGrouping)
+            {
+                this._group.AddToSubgroups(addedItem, false /*loading*/);
+            }
+
             int addedIndex = IndexOf(addedItem);
 
             // if the item is within the current page

+ 35 - 21
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@@ -17,7 +17,6 @@ namespace Avalonia.Controls
     /// </summary>
     public class DataGridCheckBoxColumn : DataGridBoundColumn
     {
-        private bool _beganEditWithKeyboard;
         private CheckBox _currentCheckBox;
         private DataGrid _owningGrid;
 
@@ -153,23 +152,7 @@ namespace Avalonia.Controls
         {
             if (editingElement is CheckBox editingCheckBox)
             {
-                bool? uneditedValue = editingCheckBox.IsChecked;
-                bool editValue = false;
-                if(editingEventArgs is PointerPressedEventArgs args)
-                {
-                    // Editing was triggered by a mouse click
-                    Point position = args.GetPosition(editingCheckBox);
-                    Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
-                    editValue = rect.Contains(position);
-                }
-                else if (_beganEditWithKeyboard)
-                {
-                    // Editing began by a user pressing spacebar
-                    editValue = true;
-                    _beganEditWithKeyboard = false;
-                }
-
-                if (editValue)
+                void EditValue()
                 {
                     // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
                     if (editingCheckBox.IsThreeState)
@@ -192,6 +175,40 @@ namespace Avalonia.Controls
                         editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
                     }
                 }
+
+                bool? uneditedValue = editingCheckBox.IsChecked;
+                if(editingEventArgs is PointerPressedEventArgs args)
+                {
+                    void ProcessPointerArgs()
+                    {
+                        // Editing was triggered by a mouse click
+                        Point position = args.GetPosition(editingCheckBox);
+                        Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
+                        if(rect.Contains(position))
+                        {
+                            EditValue();
+                        }
+                    }
+                    
+                    void OnLayoutUpdated(object sender, EventArgs e)
+                    {
+                        if(!editingCheckBox.Bounds.IsEmpty)
+                        {
+                            editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
+                            ProcessPointerArgs();
+                        }
+                    }
+
+                    if(editingCheckBox.Bounds.IsEmpty)
+                    {
+                        editingCheckBox.LayoutUpdated += OnLayoutUpdated;
+                    }
+                    else
+                    {
+                        ProcessPointerArgs();
+                    }
+                }
+
                 return uneditedValue;
             }
             return false;
@@ -284,13 +301,10 @@ namespace Avalonia.Controls
                     CheckBox checkBox = GetCellContent(row) as CheckBox;
                     if (checkBox == _currentCheckBox)
                     {
-                        _beganEditWithKeyboard = true;
                         OwningGrid.BeginEdit();
-                        return;
                     }
                 }
             }
-            _beganEditWithKeyboard = false;
         }
 
         private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@@ -8,6 +8,7 @@ using Avalonia.Controls.Utils;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Media;
+using Avalonia.Metadata;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls
@@ -22,6 +23,7 @@ namespace Avalonia.Controls
                 o => o.CellTemplate,
                 (o, v) => o.CellTemplate = v);
 
+        [Content]
         public IDataTemplate CellTemplate
         {
             get { return _cellTemplate; }

+ 5 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -1,6 +1,10 @@
 Compat issues with assembly Avalonia.Controls:
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
+EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 4
+Total Issues: 7

+ 9 - 2
src/Avalonia.Controls/Application.cs

@@ -30,7 +30,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
     {
         /// <summary>
         /// The application-global data templates.
@@ -55,6 +55,8 @@ namespace Avalonia
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
+        public event EventHandler<UrlOpenedEventArgs> UrlsOpened; 
+
         /// <summary>
         /// Creates an instance of the <see cref="Application"/> class.
         /// </summary>
@@ -247,7 +249,11 @@ namespace Avalonia
 
         public virtual void OnFrameworkInitializationCompleted()
         {
-            
+        }
+        
+        void  IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
+        {
+            UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
         }
 
         private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
@@ -288,5 +294,6 @@ namespace Avalonia
             get => _name;
             set => SetAndRaise(NameProperty, ref _name, value);
         }
+        
     }
 }

+ 14 - 0
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -5,6 +5,7 @@ using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
+using Avalonia.Platform;
 using Avalonia.Threading;
 
 namespace Avalonia.Controls.ApplicationLifetimes
@@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
         public int Start(string[] args)
         {
             Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
+
+            var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
+            
+            if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
+            {
+                ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
+            }
+
             _cts = new CancellationTokenSource();
             MainWindow?.Show();
             Dispatcher.UIThread.MainLoop(_cts.Token);
@@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
                 _activeLifetime = null;
         }
     }
+    
+    public class ClassicDesktopStyleApplicationLifetimeOptions
+    {
+        public bool ProcessUrlActivationCommandLine { get; set; }
+    }
 }
 
 namespace Avalonia

+ 5 - 3
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -483,7 +483,9 @@ namespace Avalonia.Controls
             AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
                 nameof(SelectedItem),
                 o => o.SelectedItem,
-                (o, v) => o.SelectedItem = v);
+                (o, v) => o.SelectedItem = v,
+                defaultBindingMode: BindingMode.TwoWay,
+                enableDataValidation: true);
 
         /// <summary>
         /// Identifies the
@@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
 
             base.OnApplyTemplate(e);
         }
-        
+
         /// <summary>
         /// Called to update the validation state for properties for which data validation is
         /// enabled.
@@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
         /// <param name="value">The new binding value for the property.</param>
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
-            if (property == TextProperty)
+            if (property == TextProperty || property == SelectedItemProperty)
             {
                 DataValidationErrors.SetError(this, value.Error);
             }

+ 1 - 0
src/Avalonia.Controls/Button.cs

@@ -218,6 +218,7 @@ namespace Avalonia.Controls
             if (Command != null)
             {
                 Command.CanExecuteChanged += CanExecuteChanged;
+                CanExecuteChanged(this, EventArgs.Empty);
             }
         }
 

+ 70 - 0
src/Avalonia.Controls/ComboBox.cs

@@ -10,6 +10,7 @@ using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -76,6 +77,14 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
             ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
 
+        /// <summary>
+        /// Defines the <see cref="IsTextSearchEnabled"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
+            AvaloniaProperty.Register<ComboBox, bool>(nameof(IsTextSearchEnabled), true);
+
+        private string _textSearchTerm = string.Empty;
+        private DispatcherTimer _textSearchTimer;
         private bool _isDropDownOpen;
         private Popup _popup;
         private object _selectionBoxItem;
@@ -164,6 +173,15 @@ namespace Avalonia.Controls
             set { SetValue(VerticalContentAlignmentProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value that specifies whether a user can jump to a value by typing.
+        /// </summary>
+        public bool IsTextSearchEnabled
+        {
+            get { return GetValue(IsTextSearchEnabledProperty); }
+            set { SetValue(IsTextSearchEnabledProperty, value); }
+        }
+
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
@@ -229,6 +247,32 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc />
+        protected override void OnTextInput(TextInputEventArgs e)
+        {
+            if (!IsTextSearchEnabled || e.Handled)
+                return;
+
+            StopTextSearchTimer();
+
+            _textSearchTerm += e.Text;
+
+            bool match(ItemContainerInfo info) => 
+                info.ContainerControl is IContentControl control &&
+                control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
+
+            var info = ItemContainerGenerator.Containers.FirstOrDefault(match);
+
+            if (info != null)
+            {
+                SelectedIndex = info.Index;
+            }
+
+            StartTextSearchTimer();
+
+            e.Handled = true;
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         {
@@ -426,5 +470,31 @@ namespace Avalonia.Controls
 
             SelectedIndex = prev;
         }
+
+        private void StartTextSearchTimer()
+        {
+            _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
+            _textSearchTimer.Tick += TextSearchTimer_Tick;
+            _textSearchTimer.Start();
+        }
+
+        private void StopTextSearchTimer()
+        {
+            if (_textSearchTimer == null)
+            {
+                return;
+            }
+
+            _textSearchTimer.Stop();
+            _textSearchTimer.Tick -= TextSearchTimer_Tick;
+
+            _textSearchTimer = null;
+        }
+
+        private void TextSearchTimer_Tick(object sender, EventArgs e)
+        {
+            _textSearchTerm = string.Empty;
+            StopTextSearchTimer();
+        }
     }
 }

+ 29 - 40
src/Avalonia.Controls/Control.cs

@@ -8,6 +8,8 @@ using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Controls
 {
     /// <summary>
@@ -18,25 +20,25 @@ namespace Avalonia.Controls
     ///
     /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
     /// </remarks>
-    public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue
+    public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
     {
         /// <summary>
         /// Defines the <see cref="FocusAdorner"/> property.
         /// </summary>
-        public static readonly StyledProperty<ITemplate<IControl>> FocusAdornerProperty =
-            AvaloniaProperty.Register<Control, ITemplate<IControl>>(nameof(FocusAdorner));
+        public static readonly StyledProperty<ITemplate<IControl>?> FocusAdornerProperty =
+            AvaloniaProperty.Register<Control, ITemplate<IControl>?>(nameof(FocusAdorner));
 
         /// <summary>
         /// Defines the <see cref="Tag"/> property.
         /// </summary>
-        public static readonly StyledProperty<object> TagProperty =
-            AvaloniaProperty.Register<Control, object>(nameof(Tag));
+        public static readonly StyledProperty<object?> TagProperty =
+            AvaloniaProperty.Register<Control, object?>(nameof(Tag));
         
         /// <summary>
         /// Defines the <see cref="ContextMenu"/> property.
         /// </summary>
-        public static readonly StyledProperty<ContextMenu> ContextMenuProperty =
-            AvaloniaProperty.Register<Control, ContextMenu>(nameof(ContextMenu));
+        public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
+            AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
 
         /// <summary>
         /// Event raised when an element wishes to be scrolled into view.
@@ -44,16 +46,16 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
             RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
 
-        private DataTemplates _dataTemplates;
-        private IControl _focusAdorner;
+        private DataTemplates? _dataTemplates;
+        private IControl? _focusAdorner;
 
         /// <summary>
         /// Gets or sets the control's focus adorner.
         /// </summary>
-        public ITemplate<IControl> FocusAdorner
+        public ITemplate<IControl>? FocusAdorner
         {
-            get { return GetValue(FocusAdornerProperty); }
-            set { SetValue(FocusAdornerProperty, value); }
+            get => GetValue(FocusAdornerProperty);
+            set => SetValue(FocusAdornerProperty, value);
         }
 
         /// <summary>
@@ -63,27 +65,27 @@ namespace Avalonia.Controls
         /// Each control may define data templates which are applied to the control itself and its
         /// children.
         /// </remarks>
-        public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates());
+        public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates();
 
         /// <summary>
         /// Gets or sets a context menu to the control.
         /// </summary>
-        public ContextMenu ContextMenu
+        public ContextMenu? ContextMenu
         {
-            get { return GetValue(ContextMenuProperty); }
-            set { SetValue(ContextMenuProperty, value); }
+            get => GetValue(ContextMenuProperty);
+            set => SetValue(ContextMenuProperty, value);
         }
 
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>
-        public object Tag
+        public object? Tag
         {
-            get { return GetValue(TagProperty); }
-            set { SetValue(TagProperty, value); }
+            get => GetValue(TagProperty);
+            set => SetValue(TagProperty, value);
         }
 
-        public new IControl Parent => (IControl)base.Parent;
+        public new IControl? Parent => (IControl?)base.Parent;
 
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@@ -106,15 +108,10 @@ namespace Avalonia.Controls
                     {
                         var c = i as IControl;
 
-                        if (c?.IsInitialized == false)
+                        if (c?.IsInitialized == false && c is ISupportInitialize init)
                         {
-                            var init = c as ISupportInitialize;
-
-                            if (init != null)
-                            {
-                                init.BeginInit();
-                                init.EndInit();
-                            }
+                            init.BeginInit();
+                            init.EndInit();
                         }
                     }
                 }
@@ -131,10 +128,7 @@ namespace Avalonia.Controls
         /// Gets the element that receives the focus adorner.
         /// </summary>
         /// <returns>The control that receives the focus adorner.</returns>
-        protected virtual IControl GetTemplateFocusTarget()
-        {
-            return this;
-        }
+        protected virtual IControl? GetTemplateFocusTarget() => this;
 
         /// <inheritdoc/>
         protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
@@ -173,15 +167,10 @@ namespace Avalonia.Controls
                         }
                     }
 
-                    if (_focusAdorner != null)
+                    if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target)
                     {
-                        var target = (Visual)GetTemplateFocusTarget();
-
-                        if (target != null)
-                        {
-                            AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
-                            adornerLayer.Children.Add(_focusAdorner);
-                        }
+                        AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
+                        adornerLayer.Children.Add(_focusAdorner);
                     }
                 }
             }

+ 55 - 22
src/Avalonia.Controls/DefinitionBase.cs

@@ -662,31 +662,64 @@ namespace Avalonia.Controls
                 {
                     DefinitionBase definitionBase = _registry[i];
 
-                    if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated)
+                    // we'll set d.UseSharedMinimum to maintain the invariant:
+                    //      d.UseSharedMinimum iff d._minSize < this.MinSize
+                    // i.e. iff d is not a "long-pole" definition.
+                    //
+                    // Measure/Arrange of d's Grid uses d._minSize for long-pole
+                    // definitions, and max(d._minSize, shared size) for
+                    // short-pole definitions.  This distinction allows us to react
+                    // to changes in "long-pole-ness" more efficiently and correctly,
+                    // by avoiding remeasures when a long-pole definition changes.
+                    bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize);
+
+                    // before doing that, determine whether d's Grid needs to be remeasured.
+                    // It's important _not_ to remeasure if the last measure is still
+                    // valid, otherwise infinite loops are possible
+                    bool measureIsValid;
+
+                    if(!definitionBase.UseSharedMinimum)
                     {
-                        //  if definition's min size is different, then need to re-measure
-                        if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize))
-                        {
-                            Grid parentGrid = (Grid)definitionBase.Parent;
-                            parentGrid.InvalidateMeasure();
-                            definitionBase.UseSharedMinimum = true;
-                        }
-                        else
-                        {
-                            definitionBase.UseSharedMinimum = false;
-
-                            //  if measure is valid then also need to check arrange.
-                            //  Note: definitionBase.SizeCache is volatile but at this point 
-                            //  it contains up-to-date final size
-                            if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
-                            {
-                                Grid parentGrid = (Grid)definitionBase.Parent;
-                                parentGrid.InvalidateArrange();
-                            }
-                        }
+                        // d was a long-pole.  measure is valid iff it's still a long-pole,
+                        // since previous measure didn't use shared size.
+                        measureIsValid = !useSharedMinimum;
+                    }
+                    else if(useSharedMinimum)
+                    {
+                        // d was a short-pole, and still is.  measure is valid
+                        // iff the shared size didn't change
+                        measureIsValid = !sharedMinSizeChanged;
+                    }
+                    else
+                    {
+                        // d was a short-pole, but is now a long-pole.  This can
+                        // happen in several ways:
+                        //  a. d's minSize increased to or past the old shared size
+                        //  b. other long-pole definitions decreased, leaving
+                        //      d as the new winner
+                        // In the former case, the measure is valid - it used
+                        // d's new larger minSize.  In the latter case, the
+                        // measure is invalid - it used the old shared size,
+                        // which is larger than d's (possibly changed) minSize
+                        measureIsValid = (definitionBase.LayoutWasUpdated &&
+                                        MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize));
+                    }
 
-                        definitionBase.LayoutWasUpdated = false;
+                    if(!measureIsValid)
+                    {
+                        definitionBase.Parent.InvalidateMeasure();
                     }
+                    else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
+                    {
+                        //  if measure is valid then also need to check arrange.
+                        //  Note: definitionBase.SizeCache is volatile but at this point 
+                        //  it contains up-to-date final size
+                        definitionBase.Parent.InvalidateArrange();
+                    }
+
+                    // now we can restore the invariant, and clear the layout flag
+                    definitionBase.UseSharedMinimum = useSharedMinimum;
+                    definitionBase.LayoutWasUpdated = false;
                 }
 
                 _minSize = sharedMinSize;

+ 52 - 3
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@@ -1,4 +1,10 @@
+using System;
+using System.Collections.Generic;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+using Avalonia.Reactive;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Generators
 {    
@@ -16,11 +22,15 @@ namespace Avalonia.Controls.Generators
         {
             var tabItem = (TabItem)base.CreateContainer(item);
 
-            tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
+            tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
+                tabItem,
+                TabControl.TabStripPlacementProperty));
 
             if (tabItem.HeaderTemplate == null)
             {
-                tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty];
+                tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ItemTemplateProperty));
             }
 
             if (tabItem.Header == null)
@@ -40,10 +50,49 @@ namespace Avalonia.Controls.Generators
 
             if (!(tabItem.Content is IControl))
             {
-                tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
+                tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ContentTemplateProperty));
             }
 
             return tabItem;
         }
+
+        private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
+        {
+            private readonly TabItem _item;
+            private readonly StyledProperty<T> _ownerProperty;
+            private IDisposable _ownerSubscription;
+            private IDisposable _propertySubscription;
+
+            public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
+            {
+                _item = item;
+                _ownerProperty = ownerProperty;
+            }
+
+            protected override void Subscribed()
+            {
+                _ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
+            }
+
+            protected override void Unsubscribed()
+            {
+                _ownerSubscription?.Dispose();
+                _ownerSubscription = null;
+            }
+
+            private void OwnerChanged(ILogical c)
+            {
+                _propertySubscription?.Dispose();
+                _propertySubscription = null;
+
+                if (c is TabControl tabControl)
+                {
+                    _propertySubscription = tabControl.GetObservable(_ownerProperty)
+                        .Subscribe(x => PublishNext(x));
+                }
+            }
+        }
     }
 }

+ 2 - 0
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@@ -3,5 +3,7 @@ namespace Avalonia.Controls
     public interface INativeMenuExporterEventsImplBridge
     {
         void RaiseNeedsUpdate ();
+        void RaiseOpening();
+        void RaiseClosed();
     }
 }

+ 4 - 18
src/Avalonia.Controls/NativeControlHost.cs

@@ -16,30 +16,16 @@ namespace Avalonia.Controls
         private bool _queuedForDestruction;
         private bool _queuedForMoveResize;
         private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
-        private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
-        static NativeControlHost()
-        {
-            IsVisibleProperty.Changed.AddClassHandler<NativeControlHost>(OnVisibleChanged);
-        }
-
-        public NativeControlHost()
-        {
-            _propertyChangedHandler = PropertyChangedHandler;
-        }
-
-        private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
-            => host.UpdateHost();
 
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             _currentRoot = e.Root as TopLevel;
             var visual = (IVisual)this;
-            while (visual != _currentRoot)
+            while (visual != null)
             {
-
                 if (visual is Visual v)
                 {
-                    v.PropertyChanged += _propertyChangedHandler;
+                    v.PropertyChanged += PropertyChangedHandler;
                     _propertyChangedSubscriptions.Add(v);
                 }
 
@@ -51,7 +37,7 @@ namespace Avalonia.Controls
 
         private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
         {
-            if (e.IsEffectiveValueChange && e.Property == BoundsProperty)
+            if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty))
                 EnqueueForMoveResize();
         }
 
@@ -61,7 +47,7 @@ namespace Avalonia.Controls
             if (_propertyChangedSubscriptions != null)
             {
                 foreach (var v in _propertyChangedSubscriptions)
-                    v.PropertyChanged -= _propertyChangedHandler;
+                    v.PropertyChanged -= PropertyChangedHandler;
                 _propertyChangedSubscriptions.Clear();
             }
             UpdateHost();

+ 32 - 1
src/Avalonia.Controls/NativeMenu.cs

@@ -12,13 +12,34 @@ namespace Avalonia.Controls
         private readonly AvaloniaList<NativeMenuItemBase> _items =
             new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
         private NativeMenuItem _parent;
+
         [Content]
         public IList<NativeMenuItemBase> Items => _items;
 
         /// <summary>
-        /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
+        /// Raised when the menu requests an update.
+        /// </summary>
+        /// <remarks>
+        /// Use this event to add, remove or modify menu items before a menu is
+        /// shown or a hotkey is pressed.
+        /// </remarks>
+        public event EventHandler<EventArgs> NeedsUpdate;
+
+        /// <summary>
+        /// Raised before the menu is opened.
         /// </summary>
+        /// <remarks>
+        /// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
+        /// </remarks>
         public event EventHandler<EventArgs> Opening;
+        
+        /// <summary>
+        /// Raised after the menu is closed.
+        /// </summary>
+        /// <remarks>
+        /// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
+        /// </remarks>
+        public event EventHandler<EventArgs> Closed;
 
         public NativeMenu()
         {
@@ -27,10 +48,20 @@ namespace Avalonia.Controls
         }
 
         void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
+        {
+            NeedsUpdate?.Invoke(this, EventArgs.Empty);
+        }
+
+        void INativeMenuExporterEventsImplBridge.RaiseOpening()
         {
             Opening?.Invoke(this, EventArgs.Empty);
         }
 
+        void INativeMenuExporterEventsImplBridge.RaiseClosed()
+        {
+            Closed?.Invoke(this, EventArgs.Empty);
+        }
+
         private void Validator(NativeMenuItemBase obj)
         {
             if (obj.Parent != null)

+ 16 - 0
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@@ -0,0 +1,16 @@
+using System;
+
+namespace Avalonia.Controls
+{
+
+    [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")]
+    public class NativeMenuItemSeperator : NativeMenuItemSeparator 
+    {
+    }
+
+    public class NativeMenuItemSeparator : NativeMenuItemBase
+    {
+        [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
+        public string Header => "-";
+    }
+}

+ 0 - 10
src/Avalonia.Controls/NativeMenuItemSeperator.cs

@@ -1,10 +0,0 @@
-using System;
-
-namespace Avalonia.Controls
-{
-    public class NativeMenuItemSeperator : NativeMenuItemBase
-    {
-        [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
-        public string Header => "-";
-    }
-}

+ 12 - 5
src/Avalonia.Controls/Notifications/NotificationCard.cs

@@ -16,6 +16,11 @@ namespace Avalonia.Controls.Notifications
         private bool _isClosed;
         private bool _isClosing;
 
+        static NotificationCard()
+        {
+            CloseOnClickProperty.Changed.AddClassHandler<Button>(OnCloseOnClickPropertyChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="NotificationCard"/> class.
         /// </summary>
@@ -105,22 +110,26 @@ namespace Avalonia.Controls.Notifications
 
         public static bool GetCloseOnClick(Button obj)
         {
+            Contract.Requires<ArgumentNullException>(obj != null);
             return (bool)obj.GetValue(CloseOnClickProperty);
         }
 
         public static void SetCloseOnClick(Button obj, bool value)
         {
+            Contract.Requires<ArgumentNullException>(obj != null);
             obj.SetValue(CloseOnClickProperty, value);
         }
 
         /// <summary>
         /// Defines the CloseOnClick property.
         /// </summary>
-        public static readonly AvaloniaProperty CloseOnClickProperty =
-          AvaloniaProperty.RegisterAttached<Button, bool>("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/);
+        public static readonly AttachedProperty<bool> CloseOnClickProperty =
+          AvaloniaProperty.RegisterAttached<NotificationCard, Button, bool>("CloseOnClick", defaultValue: false);
 
-        private static bool CloseOnClickChanged(Button button, bool value)
+        private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
         {
+            var button = (Button)d;
+            var value = (bool)e.NewValue;
             if (value)
             {
                 button.Click += Button_Click;
@@ -129,8 +138,6 @@ namespace Avalonia.Controls.Notifications
             {
                 button.Click -= Button_Click;
             }
-
-            return true;
         }
 
         /// <summary>

+ 16 - 2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -91,14 +91,14 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly DirectProperty<NumericUpDown, string> TextProperty =
             AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
-                defaultBindingMode: BindingMode.TwoWay);
+                defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="Value"/> property.
         /// </summary>
         public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
             AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
-                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="Watermark"/> property.
@@ -370,6 +370,20 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            if (property == TextProperty || property == ValueProperty)
+            {
+                DataValidationErrors.SetError(this, value.Error);
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="CultureInfo"/> property value changed.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// The default for the platform.
         /// </summary>
-        Default = SystemChrome,
+        Default = PreferSystemChrome,
 
         /// <summary>
         /// Use SystemChrome

+ 7 - 0
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@@ -0,0 +1,7 @@
+namespace Avalonia.Platform
+{
+    public interface IApplicationPlatformEvents
+    {
+        void RaiseUrlsOpened(string[] urls);
+    }
+}

+ 18 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
                 nameof(SelectedItem),
                 o => o.SelectedItem,
                 (o, v) => o.SelectedItem = v,
-                defaultBindingMode: BindingMode.TwoWay);
+                defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
@@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
             EndUpdating();
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            if (property == SelectedItemProperty)
+            {
+                DataValidationErrors.SetError(this, value.Error);
+            }
+        }
+        
         protected override void OnInitialized()
         {
             base.OnInitialized();
@@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
                 _oldSelectedItem = SelectedItem;
             }
             else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
-                _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
+                     _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
             {
                 RaisePropertyChanged(
                     SelectedItemsProperty,
@@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
             public Optional<ISelectionModel> Selection { get; set; }
             public Optional<IList?> SelectedItems { get; set; }
 
-            public Optional<int> SelectedIndex 
+            public Optional<int> SelectedIndex
             {
                 get => _selectedIndex;
                 set
@@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
                     _selectedIndex = default;
                 }
             }
-       }
+        }
     }
 }

+ 3 - 1
src/Avalonia.Controls/Slider.cs

@@ -341,7 +341,9 @@ namespace Avalonia.Controls
 
             var pointNum = orient ? x.Position.X : x.Position.Y;
             var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
-            var invert = orient ? 0 : 1;
+            var invert = orient ? 
+                IsDirectionReversed ? 1 : 0 :
+                IsDirectionReversed ? 0 : 1;
             var calcVal = Math.Abs(invert - logicalPos);
             var range = Maximum - Minimum;
             var finalValue = calcVal * range + Minimum;

+ 29 - 14
src/Avalonia.Controls/TextBox.cs

@@ -514,21 +514,36 @@ namespace Avalonia.Controls
 
         private void HandleTextInput(string input)
         {
-            if (!IsReadOnly)
+            if (IsReadOnly)
             {
-                input = RemoveInvalidCharacters(input);
-                string text = Text ?? string.Empty;
-                int caretIndex = CaretIndex;
-                if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength))
-                {
-                    DeleteSelection();
-                    caretIndex = CaretIndex;
-                    text = Text ?? string.Empty;
-                    SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
-                    CaretIndex += input.Length;
-                    ClearSelection();
-                    _undoRedoHelper.DiscardRedo();
-                }
+                return;
+            }
+            
+            input = RemoveInvalidCharacters(input);
+            
+            if (string.IsNullOrEmpty(input))
+            {
+                return;
+            }
+            
+            string text = Text ?? string.Empty;
+            int caretIndex = CaretIndex;
+            int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
+            
+            if (MaxLength > 0 && newLength > MaxLength)
+            {
+                input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
+            }
+            
+            if (!string.IsNullOrEmpty(input))
+            {
+                DeleteSelection();
+                caretIndex = CaretIndex;
+                text = Text ?? string.Empty;
+                SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
+                CaretIndex += input.Length;
+                ClearSelection();
+                _undoRedoHelper.DiscardRedo();
             }
         }
 

+ 14 - 0
src/Avalonia.Controls/UrlOpenedEventArgs.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia
+{
+    public class UrlOpenedEventArgs : EventArgs
+    {
+        public UrlOpenedEventArgs(string[] urls)
+        {
+            Urls = urls;
+        }
+        
+        public string[] Urls { get; }
+    }
+}

+ 25 - 20
src/Avalonia.Controls/Window.cs

@@ -4,10 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
-using Avalonia.Controls.Chrome;
 using Avalonia.Controls.Platform;
-using Avalonia.Controls.Primitives;
-using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
@@ -482,10 +479,9 @@ namespace Avalonia.Controls
 
             try
             {
-                if (!ignoreCancel && HandleClosing())
+                if (!ignoreCancel && ShouldCancelClose())
                 {
                     close = false;
-                    return;
                 }
             }
             finally
@@ -497,11 +493,25 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
+        /// <returns>true if closing is cancelled. Otherwise false.</returns>
+        /// </summary>
+        protected virtual bool HandleClosing()
+        {
+            if (!ShouldCancelClose())
+            {
+                CloseInternal();
+                return false;
+            }
+            
+            return true;
+        }
+
         private void CloseInternal()
         {
             foreach (var (child, _) in _children.ToList())
             {
-                // if we HandleClosing() before then there will be no children.
                 child.CloseInternal();
             }
 
@@ -515,20 +525,18 @@ namespace Avalonia.Controls
             PlatformImpl?.Dispose();
         }
 
-        /// <summary>
-        /// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
-        /// </summary>
-        protected virtual bool HandleClosing()
+        private bool ShouldCancelClose(CancelEventArgs args = null)
         {
+            if (args is null)
+            {
+                args = new CancelEventArgs();
+            }
+            
             bool canClose = true;
 
             foreach (var (child, _) in _children.ToList())
             {
-                if (!child.HandleClosing())
-                {
-                    child.CloseInternal();
-                }
-                else
+                if (child.ShouldCancelClose(args))
                 {
                     canClose = false;
                 }
@@ -536,15 +544,12 @@ namespace Avalonia.Controls
 
             if (canClose)
             {
-                var args = new CancelEventArgs();
                 OnClosing(args);
 
                 return args.Cancel;
             }
-            else
-            {
-                return !canClose;
-            }
+
+            return true;
         }
 
         protected virtual void HandleWindowStateChanged(WindowState state)

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote
             if (entryPoint == null)
                 throw Die($"Assembly {args.AppPath} doesn't have an entry point");
             var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
-                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
             if (builderMethod == null)
                 throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
             Design.IsDesignMode = true;

+ 21 - 0
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+    internal class BoolToOpacityConverter : IValueConverter
+    {
+        public double Opacity { get; set; }
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return (bool)value ? 1d : Opacity;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 225 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -1,8 +1,15 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Linq;
+using System.Reflection;
 using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
@@ -12,6 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly IVisual _control;
         private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
         private AvaloniaPropertyViewModel _selectedProperty;
+        private string _styleFilter;
+        private bool _snapshotStyles;
+        private bool _showInactiveStyles;
+        private string _styleStatus;
 
         public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
         {
@@ -43,20 +54,160 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 ao.PropertyChanged += ControlPropertyChanged;
             }
+
+            AppliedStyles = new ObservableCollection<StyleViewModel>();
+            PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
+
+            if (control is StyledElement styledElement)
+            {
+                styledElement.Classes.CollectionChanged += OnClassesChanged;
+
+                var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(true);
+
+                foreach (var classAttribute in pseudoClassAttributes)
+                {
+                    foreach (var className in classAttribute.PseudoClasses)
+                    {
+                        PseudoClasses.Add(new PseudoClassViewModel(className, styledElement));
+                    }
+                }
+
+                var styleDiagnostics = styledElement.GetStyleDiagnostics();
+
+                foreach (var appliedStyle in styleDiagnostics.AppliedStyles)
+                {
+                    var styleSource = appliedStyle.Source;
+
+                    var setters = new List<SetterViewModel>();
+
+                    if (styleSource is Style style)
+                    {
+                        foreach (var setter in style.Setters)
+                        {
+                            if (setter is Setter regularSetter)
+                            {
+                                var setterValue = regularSetter.Value;
+
+                                var resourceInfo = GetResourceInfo(setterValue);
+
+                                SetterViewModel setterVm;
+
+                                if (resourceInfo.HasValue)
+                                {
+                                    var resourceKey = resourceInfo.Value.resourceKey;
+                                    var resourceValue = styledElement.FindResource(resourceKey);
+
+                                    setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
+                                }
+                                else
+                                {
+                                    setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+                                }
+
+                                setters.Add(setterVm);
+                            }
+                        }
+
+                        AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
+                    }
+                }
+
+                UpdateStyles();
+            }
+        }
+
+        private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
+        {
+            if (value is StaticResourceExtension staticResource)
+            {
+                return (staticResource.ResourceKey, false);
+            }
+            else if (value is DynamicResourceExtension dynamicResource)
+            {
+                return (dynamicResource.ResourceKey, true);
+            }
+
+            return null;
         }
 
         public TreePageViewModel TreePage { get; }
 
         public DataGridCollectionView PropertiesView { get; }
 
+        public ObservableCollection<StyleViewModel> AppliedStyles { get; }
+
+        public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
+
         public AvaloniaPropertyViewModel SelectedProperty
         {
             get => _selectedProperty;
             set => RaiseAndSetIfChanged(ref _selectedProperty, value);
         }
-        
+
+        public string StyleFilter
+        {
+            get => _styleFilter;
+            set => RaiseAndSetIfChanged(ref _styleFilter, value);
+        }
+
+        public bool SnapshotStyles
+        {
+            get => _snapshotStyles;
+            set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
+        }
+
+        public bool ShowInactiveStyles
+        {
+            get => _showInactiveStyles;
+            set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
+        }
+
+        public string StyleStatus
+        {
+            get => _styleStatus;
+            set => RaiseAndSetIfChanged(ref _styleStatus, value);
+        }
+
         public ControlLayoutViewModel Layout { get; }
 
+        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            base.OnPropertyChanged(e);
+
+            if (e.PropertyName == nameof(StyleFilter))
+            {
+                UpdateStyleFilters();
+            }
+            else if (e.PropertyName == nameof(SnapshotStyles))
+            {
+                if (!SnapshotStyles)
+                {
+                    UpdateStyles();
+                }
+            }
+        }
+
+        private void UpdateStyleFilters()
+        {
+            var filter = StyleFilter;
+            bool hasFilter = !string.IsNullOrEmpty(filter);
+
+            foreach (var style in AppliedStyles)
+            {
+                var hasVisibleSetter = false;
+
+                foreach (var setter in style.Setters)
+                {
+                    setter.IsVisible =
+                        !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+
+                    hasVisibleSetter |= setter.IsVisible;
+                }
+
+                style.IsVisible = hasVisibleSetter;
+            }
+        }
+
         public void Dispose()
         {
             if (_control is INotifyPropertyChanged inpc)
@@ -68,6 +219,11 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 ao.PropertyChanged -= ControlPropertyChanged;
             }
+
+            if (_control is StyledElement se)
+            {
+                se.Classes.CollectionChanged -= OnClassesChanged;
+            }
         }
 
         private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
@@ -129,6 +285,74 @@ namespace Avalonia.Diagnostics.ViewModels
                     property.Update();
                 }
             }
+
+            if (!SnapshotStyles)
+            {
+                UpdateStyles();
+            }
+        }
+
+        private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (!SnapshotStyles)
+            {
+                UpdateStyles();
+            }
+        }
+
+        private void UpdateStyles()
+        {
+            int activeCount = 0;
+
+            foreach (var style in AppliedStyles)
+            {
+                style.Update();
+
+                if (style.IsActive)
+                {
+                    activeCount++;
+                }
+            }
+
+            var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>();
+
+            foreach (var style in AppliedStyles)
+            {
+                if (!style.IsActive)
+                {
+                    continue;
+                }
+
+                foreach (var setter in style.Setters)
+                {
+                    if (propertyBuckets.TryGetValue(setter.Property, out var setters))
+                    {
+                        foreach (var otherSetter in setters)
+                        {
+                            otherSetter.IsActive = false;
+                        }
+
+                        setter.IsActive = true;
+
+                        setters.Add(setter);
+                    }
+                    else
+                    {
+                        setter.IsActive = true;
+
+                        setters = new List<SetterViewModel> { setter };
+
+                        propertyBuckets.Add(setter.Property, setters);
+                    }
+                }
+            }
+
+            foreach (var pseudoClass in PseudoClasses)
+            {
+                pseudoClass.Update();
+            }
+
+            StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
         }
 
         private bool FilterProperty(object arg)

+ 8 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -163,6 +163,14 @@ namespace Avalonia.Diagnostics.ViewModels
             tree?.SelectControl(control);
         }
 
+        public void EnableSnapshotStyles(bool enable)
+        {
+            if (Content is TreePageViewModel treeVm && treeVm.Details != null)
+            {
+                treeVm.Details.SnapshotStyles = enable;
+            }
+        }
+
         public void Dispose()
         {
             KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;

+ 51 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs

@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class PseudoClassViewModel : ViewModelBase
+    {
+        private readonly IPseudoClasses _pseudoClasses;
+        private readonly StyledElement _source;
+        private bool _isActive;
+        private bool _isUpdating;
+
+        public PseudoClassViewModel(string name, StyledElement source)
+        {
+            Name = name;
+            _source = source;
+            _pseudoClasses = _source.Classes;
+
+            Update();
+        }
+
+        public string Name { get; }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set
+            {
+                RaiseAndSetIfChanged(ref _isActive, value);
+
+                if (!_isUpdating)
+                {
+                    _pseudoClasses.Set(Name, value);
+                }
+            }
+        }
+
+        public void Update()
+        {
+            try
+            {
+                _isUpdating = true;
+
+                IsActive = _source.Classes.Contains(Name);
+            }
+            finally
+            {
+                _isUpdating = false;
+            }
+        }
+    }
+}

+ 27 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@@ -0,0 +1,27 @@
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ResourceSetterViewModel : SetterViewModel
+    {
+        public object Key { get; }
+
+        public IBrush Tint { get; }
+
+        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
+        {
+            Key = resourceKey;
+            Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
+        }
+
+        public void CopyResourceKey()
+        {
+            if (Key is null)
+            {
+                return;
+            }
+
+            CopyToClipboard(Key.ToString());
+        }
+    }
+}

+ 59 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@@ -0,0 +1,59 @@
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class SetterViewModel : ViewModelBase
+    {
+        private bool _isActive;
+        private bool _isVisible;
+
+        public AvaloniaProperty Property { get; }
+
+        public string Name { get; }
+
+        public object Value { get; }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => RaiseAndSetIfChanged(ref _isActive, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public SetterViewModel(AvaloniaProperty property, object value)
+        {
+            Property = property;
+            Name = property.Name;
+            Value = value;
+            IsActive = true;
+            IsVisible = true;
+        }
+
+        public void CopyValue()
+        {
+            if (Value is null)
+            {
+                return;
+            }
+
+            CopyToClipboard(Value.ToString());
+        }
+
+        public void CopyPropertyName()
+        {
+            CopyToClipboard(Property.Name);
+        }
+
+        protected static void CopyToClipboard(string value)
+        {
+            var clipboard = AvaloniaLocator.Current.GetService<IClipboard>();
+
+            clipboard?.SetTextAsync(value);
+        }
+    }
+}

+ 43 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs

@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class StyleViewModel : ViewModelBase
+    {
+        private readonly IStyleInstance _styleInstance;
+        private bool _isActive;
+        private bool _isVisible;
+
+        public StyleViewModel(IStyleInstance styleInstance, string name, List<SetterViewModel> setters)
+        {
+            _styleInstance = styleInstance;
+            IsVisible = true;
+            Name = name;
+            Setters = setters;
+
+            Update();
+        }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => RaiseAndSetIfChanged(ref _isActive, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public string Name { get; }
+
+        public List<SetterViewModel> Setters { get; }
+
+        public void Update()
+        {
+            IsActive = _styleInstance.IsActive;
+        }
+    }
+}

+ 7 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -31,11 +31,18 @@ namespace Avalonia.Diagnostics.ViewModels
             get => _selectedNode;
             private set
             {
+                var oldDetails = Details;
+
                 if (RaiseAndSetIfChanged(ref _selectedNode, value))
                 {
                     Details = value != null ?
                         new ControlDetailsViewModel(this, value.Visual) :
                         null;
+
+                    if (Details != null && oldDetails != null)
+                    {
+                        Details.StyleFilter = oldDetails.StyleFilter;
+                    }
                 }
             }
         }

+ 121 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -2,7 +2,9 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
              xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
-             x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
+             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
+             x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
+             x:Name="Main">
 
   <UserControl.Resources>
     <SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" />
@@ -11,6 +13,7 @@
     <SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" />
     <SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" />
     <SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" />
+    <conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
   </UserControl.Resources>
 
   <UserControl.Styles>
@@ -105,7 +108,7 @@
 
     <GridSplitter Grid.Column="1" />
 
-    <Grid Grid.Column="2" RowDefinitions="Auto,*" >
+    <Grid Grid.Column="2" RowDefinitions="Auto,*, Auto,*,Auto" >
       <TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" />
       
       <Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
@@ -148,7 +151,122 @@
           <Rectangle x:Name="VerticalSizeEnd" />
         </Canvas>
       </Grid>
-      
+
+      <Grid Grid.Row="2" Margin="4" RowDefinitions="Auto,Auto">
+
+        <Grid Grid.Row="0" Margin="2" ColumnDefinitions="Auto,*,Auto,Auto">
+          <TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding StyleStatus}" VerticalAlignment="Center" />
+          <CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveStyles}" ToolTip.Tip="Show styles that are currently inactive" />
+          <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
+        </Grid>
+
+        <TextBox Grid.Row="1" Margin="2" Grid.Column="0" Watermark="Filter" Text="{Binding StyleFilter}" />
+      </Grid>
+
+        <ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
+        <ItemsControl Items="{Binding AppliedStyles}" >
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
+                <Border.IsVisible>
+                  <MultiBinding Converter="{x:Static BoolConverters.And}">
+                    <MultiBinding Converter="{x:Static BoolConverters.Or}" >
+                      <Binding Path="IsActive" />
+                      <Binding Path="#Main.DataContext.ShowInactiveStyles" />
+                    </MultiBinding>
+                    <Binding Path="IsVisible" />
+                  </MultiBinding>
+                </Border.IsVisible>
+                <Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" >
+                  <Expander.Header>
+                    <TextBlock Grid.Row="0" Text="{Binding Name}" />
+                  </Expander.Header>
+
+                  <ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}">
+                    <ItemsControl.DataTemplates>
+
+                      <DataTemplate DataType="IBrush">
+                        <StackPanel Orientation="Horizontal" Spacing="2">
+                          <Border BorderThickness="1" BorderBrush="Black" Background="{Binding}" Width="8" Height="8"/>
+                          <TextBlock Text="{Binding}" />
+                        </StackPanel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="Color">
+                        <StackPanel Orientation="Horizontal" Spacing="2">
+                          <Border BorderThickness="1" BorderBrush="Black" Width="8" Height="8">
+                            <Border.Background>
+                              <SolidColorBrush Color="{Binding}" />
+                            </Border.Background>
+                          </Border>
+                          <TextBlock Text="{Binding}" />
+                        </StackPanel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="vm:ResourceSetterViewModel">
+                        <Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
+                          <Panel.ContextMenu>
+                            <ContextMenu>
+                              <MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
+                              <MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
+                              <MenuItem Header="Copy resource key" Command="{Binding CopyResourceKey}" />
+                            </ContextMenu>
+                          </Panel.ContextMenu>
+                          <StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Left">
+                            <TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
+                            <TextBlock Text=":" />
+                            <ContentControl Content="{Binding Value}"/>
+                            <TextBlock>(</TextBlock>
+                            <Ellipse Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}"/>
+                            <TextBlock FontStyle="Italic" Text="{Binding Key}" />
+                            <TextBlock>)</TextBlock>
+                          </StackPanel>
+                          <Rectangle Height="1" Fill="#6C6C6C" IsVisible="{Binding !IsActive}" />
+                        </Panel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="vm:SetterViewModel">
+                        <Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
+                          <Panel.ContextMenu>
+                            <ContextMenu>
+                              <MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
+                              <MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
+                            </ContextMenu>
+                          </Panel.ContextMenu>
+                          <StackPanel Orientation="Horizontal" Spacing="2">
+                            <TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
+                            <TextBlock Text=":" />
+                            <ContentControl Content="{Binding Value}"/>
+                          </StackPanel>
+                          <Rectangle Height="1" Fill="#6C6C6C" VerticalAlignment="Center" IsVisible="{Binding !IsActive}" />
+                        </Panel>
+                      </DataTemplate>
+
+                    </ItemsControl.DataTemplates>
+                  </ItemsControl>
+
+                </Expander>
+              </Border>
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </ScrollViewer>
+
+      <Expander Header="Pseudo Classes" Grid.Row="4">
+        <ItemsControl Items="{Binding PseudoClasses}">
+          <ItemsControl.ItemsPanel>
+            <ItemsPanelTemplate>
+              <WrapPanel />
+            </ItemsPanelTemplate>
+          </ItemsControl.ItemsPanel>
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <CheckBox Margin="2" Content="{Binding Name}" IsChecked="{Binding IsActive, Mode=TwoWay}" />
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </Expander>
+
     </Grid>
   </Grid>
 

+ 10 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -90,6 +90,16 @@ namespace Avalonia.Diagnostics.Views
                     var vm = (MainViewModel)DataContext;
                     vm.SelectControl((IControl)control);
                 }
+            } 
+            else if (e.Modifiers == RawInputModifiers.Alt)
+            {
+                if (e.Key == Key.S || e.Key == Key.D)
+                {
+                    var enable = e.Key == Key.S;
+
+                    var vm = (MainViewModel)DataContext;
+                    vm.EnableSnapshotStyles(enable);
+                }
             }
         }
 

+ 1 - 1
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop
             {
                 var (it, menu) = i;
 
-                if (it is NativeMenuItemSeperator)
+                if (it is NativeMenuItemSeparator)
                 {
                     if (name == "type")
                         return "separator";

+ 1 - 1
src/Avalonia.Layout/ElementManager.cs

@@ -207,7 +207,7 @@ namespace Avalonia.Layout
             }
         }
 
-        public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
+        public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount;
 
         public ILayoutable GetRealizedElement(int dataIndex)
         {

+ 1 - 1
src/Avalonia.Layout/UniformGridLayout.cs

@@ -447,7 +447,7 @@ namespace Avalonia.Layout
             // and only use the layout when to clear it when it's done.
             gridState.EnsureFirstElementOwnership(context);
 
-            return new Size(desiredSize.Width, desiredSize.Height);
+            return desiredSize;
         }
 
         protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)

+ 5 - 5
src/Avalonia.Layout/UniformGridLayoutState.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Layout
             Size availableSize,
             VirtualizingLayoutContext context,
             double layoutItemWidth,
-            double LayoutItemHeight,
+            double layoutItemHeight,
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
             double minRowSpacing,
@@ -63,7 +63,7 @@ namespace Avalonia.Layout
                 if (realizedElement != null)
                 {
                     realizedElement.Measure(availableSize);
-                    SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
+                    SetSize(realizedElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
                     _cachedFirstElement = null;
                 }
                 else
@@ -78,7 +78,7 @@ namespace Avalonia.Layout
 
                     _cachedFirstElement.Measure(availableSize);
 
-                    SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
+                    SetSize(_cachedFirstElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
 
                     // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
                     bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
@@ -93,7 +93,7 @@ namespace Avalonia.Layout
         private void SetSize(
             ILayoutable element,
             double layoutItemWidth,
-            double LayoutItemHeight,
+            double layoutItemHeight,
             Size availableSize,
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
@@ -107,7 +107,7 @@ namespace Avalonia.Layout
             }
 
             EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
-            EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
+            EffectiveItemHeight = (double.IsNaN(layoutItemHeight) ? element.DesiredSize.Height : layoutItemHeight);
 
             var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
             var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;

+ 14 - 0
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Native.Interop;
+using Avalonia.Platform;
+
+namespace Avalonia.Native
+{
+    internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents
+    {
+        void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
+        {
+            ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray());
+        }
+    }
+}

+ 6 - 3
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Runtime.InteropServices;
-using System.Security.Cryptography;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
@@ -9,7 +8,6 @@ using Avalonia.Native.Interop;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
-using Avalonia.Platform.Interop;
 
 namespace Avalonia.Native
 {
@@ -86,12 +84,17 @@ namespace Avalonia.Native
         void DoInitialize(AvaloniaNativePlatformOptions options)
         {
             _options = options;
-            _factory.Initialize(new GCHandleDeallocator());
+            
+            var applicationPlatform = new AvaloniaNativeApplicationPlatform();
+            
+            _factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
             if (_factory.MacOptions != null)
             {
                 var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
 
                 _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0);
+                _factory.MacOptions.SetDisableDefaultApplicationMenuItems(
+                    macOpts?.DisableDefaultApplicationMenuItems == true ? 1 : 0);
             }
 
             AvaloniaLocator.CurrentMutable

+ 2 - 0
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@@ -38,5 +38,7 @@ namespace Avalonia
     public class MacOSPlatformOptions
     {
         public bool ShowInDock { get; set; } = true;
+        
+        public bool DisableDefaultApplicationMenuItems { get; set; }
     }
 }

+ 24 - 2
src/Avalonia.Native/IAvnMenu.cs

@@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
         {
             _parent?.RaiseNeedsUpdate();
         }
+
+        public void Opening()
+        {
+            _parent?.RaiseOpening();
+        }
+
+        public void Closed()
+        {
+            _parent?.RaiseClosed();
+        }
     }
 
     partial interface IAvnMenu
     {
         void RaiseNeedsUpdate();
+        void RaiseOpening();
+        void RaiseClosed();
         void Deinitialise();
     }
 }
@@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
             _exporter.UpdateIfNeeded();
         }
 
+        public void RaiseOpening()
+        {
+            (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
+        }
+
+        public void RaiseClosed()
+        {
+            (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
+        }
+
         internal NativeMenu ManagedMenu { get; private set; }
 
         public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)
@@ -103,8 +125,8 @@ namespace Avalonia.Native.Interop.Impl
 
         private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
         {
-            var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ?
-                factory.CreateMenuItemSeperator() :
+            var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ?
+                factory.CreateMenuItemSeparator() :
                 factory.CreateMenuItem());
             nativeItem.ManagedMenuItem = item;
 

+ 12 - 6
src/Avalonia.Native/avn.idl

@@ -397,13 +397,13 @@ enum AvnExtendClientAreaChromeHints
     AvnSystemChrome = 0x01,
     AvnPreferSystemChrome = 0x02,
     AvnOSXThickTitleBar = 0x08,
-    AvnDefaultChrome = AvnSystemChrome,
+    AvnDefaultChrome = AvnPreferSystemChrome,
 }
 
 [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
 interface IAvaloniaNativeFactory : IUnknown
 {
-     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator);
+     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
      IAvnMacOptions* GetMacOptions();
      HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
      HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown
      HRESULT SetAppMenu(IAvnMenu* menu);
      HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
      HRESULT CreateMenuItem(IAvnMenuItem** ppv);
-     HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv);
+     HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
 }
 
 [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@@ -528,6 +528,7 @@ interface IAvnMacOptions : IUnknown
 {
      HRESULT SetShowInDock(int show);
      HRESULT SetApplicationTitle(char* utf8string);
+     HRESULT SetDisableDefaultApplicationMenuItems(bool enabled);
 }
 
 [uuid(04c1b049-1f43-418a-9159-cae627ec1367)]
@@ -684,10 +685,9 @@ interface IAvnMenuItem : IUnknown
 [uuid(0af7df53-7632-42f4-a650-0992c361b477)]
 interface IAvnMenuEvents : IUnknown
 {
-    /**
-     * NeedsUpdate
-     */
      void NeedsUpdate();
+     void Opening();
+     void Closed();
 }
 
 [uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]
@@ -727,3 +727,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
      void HideWithSize(float width, float height);
      void ReleaseChild();
 }
+
+[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
+interface IAvnApplicationEvents : IUnknown
+{
+     void FilesOpened (IAvnStringArray* urls);
+}

+ 11 - 7
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI
     {
         /// <summary>
         /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia 
-        /// scheduler and Avalonia activation for view fetcher. Always remember to
-        /// call this method if you are using ReactiveUI in your application.
+        /// scheduler, an activation for view fetcher, a template binding hook. Remember
+        /// to call this method if you are using ReactiveUI in your application.
         /// </summary>
         public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
-            where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
-        {
-            return builder.AfterPlatformServicesSetup(_ =>
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new() =>
+            builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() =>
             {
+                if (Locator.CurrentMutable is null)
+                {
+                    return;
+                }
+
+                PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia);
                 RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
                 Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
                 Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
-            });
-        }
+            }));
     }
 }

+ 4 - 0
src/Avalonia.Styling/ApiCompatBaseline.txt

@@ -0,0 +1,4 @@
+Compat issues with assembly Avalonia.Styling:
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract.
+Total Issues: 2

+ 21 - 0
src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs

@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics
+{
+    /// <summary>
+    /// Contains information about style related diagnostics of a control.
+    /// </summary>
+    public class StyleDiagnostics
+    {
+        /// <summary>
+        /// Currently applied styles.
+        /// </summary>
+        public IReadOnlyList<IStyleInstance> AppliedStyles { get; }
+
+        public StyleDiagnostics(IReadOnlyList<IStyleInstance> appliedStyles)
+        {
+            AppliedStyles = appliedStyles;
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs

@@ -0,0 +1,17 @@
+namespace Avalonia.Diagnostics
+{
+    /// <summary>
+    /// Defines diagnostic extensions on <see cref="StyledElement"/>s.
+    /// </summary>
+    public static class StyledElementExtensions
+    {
+        /// <summary>
+        /// Gets a style diagnostics for a <see cref="StyledElement"/>.
+        /// </summary>
+        /// <param name="styledElement">The element.</param>
+        public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
+        {
+            return styledElement.GetStyleDiagnosticsInternal();
+        }
+    }
+}

+ 3 - 1
src/Avalonia.Styling/IStyledElement.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
@@ -10,7 +11,8 @@ namespace Avalonia
         IStyleHost,
         ILogical,
         IResourceHost,
-        IDataContextProvider
+        IDataContextProvider,
+        ISupportInitialize
     {
         /// <summary>
         /// Occurs when the control has finished initialization.

+ 35 - 5
src/Avalonia.Styling/StyledElement.cs

@@ -334,7 +334,16 @@ namespace Avalonia
         {
             if (_initCount == 0 && !_styled)
             {
-                AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                try
+                {
+                    BeginBatchUpdate();
+                    AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
+
                 _styled = true;
             }
 
@@ -356,6 +365,18 @@ namespace Avalonia
             }
         }
 
+        internal StyleDiagnostics GetStyleDiagnosticsInternal()
+        {
+            IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles;
+
+            if (appliedStyles is null)
+            {
+                appliedStyles = Array.Empty<IStyleInstance>();
+            }
+
+            return new StyleDiagnostics(appliedStyles);
+        }
+
         /// <inheritdoc/>
         void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
@@ -736,12 +757,21 @@ namespace Avalonia
         {
             if (_appliedStyles is object)
             {
-                foreach (var i in _appliedStyles)
+                BeginBatchUpdate();
+
+                try
                 {
-                    i.Dispose();
-                }
+                    foreach (var i in _appliedStyles)
+                    {
+                        i.Dispose();
+                    }
 
-                _appliedStyles.Clear();
+                    _appliedStyles.Clear();
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
             }
 
             _styled = false;

+ 5 - 0
src/Avalonia.Styling/Styling/IStyleInstance.cs

@@ -14,6 +14,11 @@ namespace Avalonia.Styling
         /// </summary>
         IStyle Source { get; }
 
+        /// <summary>
+        /// Gets a value indicating whether this style is active.
+        /// </summary>
+        bool IsActive { get; }
+
         /// <summary>
         /// Instructs the style to start acting upon the control.
         /// </summary>

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