Просмотр исходного кода

Merge branch 'master' into fix-adorner-measure

Dan Walmsley 4 лет назад
Родитель
Сommit
22cf20fdf3
100 измененных файлов с 1932 добавлено и 812 удалено
  1. 2 1
      build/MicroCom.targets
  2. 1 0
      native/Avalonia.Native/src/OSX/KeyTransform.mm
  3. 6 0
      native/Avalonia.Native/src/OSX/app.mm
  4. 22 0
      native/Avalonia.Native/src/OSX/window.h
  5. 91 60
      native/Avalonia.Native/src/OSX/window.mm
  6. 0 102
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  7. 0 45
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
  8. 157 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
  9. 91 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  10. 85 55
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  11. 39 3
      samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
  12. 15 10
      samples/ControlCatalog/Pages/DataGridPage.xaml
  13. 0 1
      samples/ControlCatalog/SideBar.xaml
  14. 0 78
      samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs
  15. 2 2
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  16. 149 0
      samples/RenderDemo/Pages/AnimationsPage.xaml
  17. 80 4
      samples/RenderDemo/Pages/TransitionsPage.xaml
  18. 0 1
      samples/RenderDemo/SideBar.xaml
  19. 2 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  20. 1 1
      src/Avalonia.Base/AvaloniaObject.cs
  21. 2 2
      src/Avalonia.Base/AvaloniaProperty.cs
  22. 2 2
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  23. 1 1
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  24. 6 0
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  25. 1 1
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  26. 3 3
      src/Avalonia.Base/DirectPropertyBase.cs
  27. 1 1
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  28. 4 4
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  29. 50 10
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  30. 10 2
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  31. 1 1
      src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
  32. 1 1
      src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
  33. 26 18
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  34. 4 4
      src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
  35. 3 3
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  36. 9 9
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  37. 1 1
      src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
  38. 13 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  39. 5 1
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  40. 7 3
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  41. 22 2
      src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs
  42. 18 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  43. 1 1
      src/Avalonia.Controls/AppBuilderBase.cs
  44. 28 1
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  45. 19 0
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  46. 9 0
      src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs
  47. 2 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  48. 1 0
      src/Avalonia.Controls/ComboBox.cs
  49. 61 19
      src/Avalonia.Controls/ContextMenu.cs
  50. 58 0
      src/Avalonia.Controls/ContextRequestedEventArgs.cs
  51. 62 0
      src/Avalonia.Controls/Control.cs
  52. 5 5
      src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
  53. 2 2
      src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
  54. 1 1
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  55. 6 6
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  56. 4 4
      src/Avalonia.Controls/DefinitionBase.cs
  57. 23 0
      src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
  58. 15 0
      src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
  59. 2 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  60. 132 53
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  61. 0 9
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  62. 1 1
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  63. 1 9
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  64. 9 9
      src/Avalonia.Controls/Grid.cs
  65. 1 1
      src/Avalonia.Controls/ListBox.cs
  66. 1 1
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  67. 1 1
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  68. 17 0
      src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs
  69. 36 1
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  70. 3 1
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  71. 3 1
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  72. 4 2
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  73. 48 19
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  74. 0 5
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  75. 25 1
      src/Avalonia.Controls/Primitives/Popup.cs
  76. 1 1
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  77. 3 6
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  78. 1 1
      src/Avalonia.Controls/Primitives/RangeBase.cs
  79. 32 2
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  80. 2 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  81. 29 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  82. 20 0
      src/Avalonia.Controls/Primitives/Thumb.cs
  83. 1 1
      src/Avalonia.Controls/Repeater/IElementFactory.cs
  84. 2 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  85. 1 1
      src/Avalonia.Controls/Repeater/ViewManager.cs
  86. 1 1
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  87. 1 1
      src/Avalonia.Controls/ScrollViewer.cs
  88. 1 1
      src/Avalonia.Controls/Selection/SelectionModel.cs
  89. 61 25
      src/Avalonia.Controls/TextBox.cs
  90. 4 4
      src/Avalonia.Controls/TickBar.cs
  91. 25 15
      src/Avalonia.Controls/ToolTip.cs
  92. 7 2
      src/Avalonia.Controls/TopLevel.cs
  93. 2 9
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  94. 77 68
      src/Avalonia.Controls/Window.cs
  95. 12 22
      src/Avalonia.Controls/WindowBase.cs
  96. 2 6
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  97. 4 4
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  98. 7 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  99. 13 20
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  100. 109 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

+ 2 - 1
build/MicroCom.targets

@@ -15,7 +15,8 @@
           Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs"
           Outputs="%(AvnComIdl.OutputFile)">
     <Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" />
-    <Exec Command="dotnet $(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)" LogStandardErrorAsError="true" />
+    <Exec Command="dotnet &quot;$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll&quot; -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)" 
+          LogStandardErrorAsError="true" />
     <ItemGroup>
       <!-- Remove and re-add generated file, this is needed for the clean build -->
       <Compile Remove="%(AvnComIdl.OutputFile)"/>

+ 1 - 0
native/Avalonia.Native/src/OSX/KeyTransform.mm

@@ -222,6 +222,7 @@ std::map<int, const char*> s_QwertyKeyMap =
     { 45, "n" },
     { 46, "m" },
     { 47, "." },
+    { 48, "\t" },
     { 49, " " },
     { 50, "`" },
     { 51, "" },

+ 6 - 0
native/Avalonia.Native/src/OSX/app.mm

@@ -50,6 +50,12 @@ ComPtr<IAvnApplicationEvents> _events;
     
     _events->FilesOpened(array);
 }
+
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
+{
+    return _events->TryShutdown() ? NSTerminateNow : NSTerminateCancel;
+}
+
 @end
 
 @interface AvnApplication : NSApplication

+ 22 - 0
native/Avalonia.Native/src/OSX/window.h

@@ -10,6 +10,8 @@ class WindowBaseImpl;
 -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
 -(void) onClosed;
 -(AvnPixelSize) getPixelSize;
+-(AvnPlatformResizeReason) getResizeReason;
+-(void) setResizeReason:(AvnPlatformResizeReason)reason;
 @end
 
 @interface AutoFitContentView : NSView
@@ -34,6 +36,7 @@ class WindowBaseImpl;
 -(double) getScaling;
 -(double) getExtendedTitleBarHeight;
 -(void) setIsExtended:(bool)value;
+-(bool) isDialog;
 @end
 
 struct INSWindowHolder
@@ -50,4 +53,23 @@ struct IWindowStateChanged
     virtual AvnWindowState WindowState () = 0;
 };
 
+class ResizeScope
+{
+public:
+    ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason)
+    {
+        _view = view;
+        _restore = [view getResizeReason];
+        [view setResizeReason:reason];
+    }
+    
+    ~ResizeScope()
+    {
+        [_view setResizeReason:_restore];
+    }
+private:
+    AvnView* _Nonnull _view;
+    AvnPlatformResizeReason _restore;
+};
+
 #endif /* window_h */

+ 91 - 60
native/Avalonia.Native/src/OSX/window.mm

@@ -29,10 +29,12 @@ public:
     IAvnMenu* _mainMenu;
     
     bool _shown;
+    bool _inResize;
     
     WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
     {
         _shown = false;
+        _inResize = false;
         _mainMenu = nullptr;
         BaseEvents = events;
         _glContext = gl;
@@ -50,6 +52,7 @@ public:
         [Window setBackingType:NSBackingStoreBuffered];
         
         [Window setOpaque:false];
+        [Window setContentView: StandardContainer];
     }
     
     virtual HRESULT ObtainNSWindowHandle(void** ret) override
@@ -113,7 +116,7 @@ public:
         return Window;
     }
     
-    virtual HRESULT Show(bool activate) override
+    virtual HRESULT Show(bool activate, bool isDialog) override
     {
         START_COM_CALL;
         
@@ -122,7 +125,6 @@ public:
             SetPosition(lastPositionSet);
             UpdateStyle();
             
-            [Window setContentView: StandardContainer];
             [Window setTitle:_lastTitle];
             
             if(ShouldTakeFocusOnShow() && activate)
@@ -275,9 +277,17 @@ public:
         }
     }
     
-    virtual HRESULT Resize(double x, double y) override
+    virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override
     {
+        if(_inResize)
+        {
+            return S_OK;
+        }
+        
+        _inResize = true;
+        
         START_COM_CALL;
+        auto resizeBlock = ResizeScope(View, reason);
         
         @autoreleasepool
         {
@@ -304,13 +314,19 @@ public:
                 y = maxSize.height;
             }
             
-            if(!_shown)
+            @try
             {
-                BaseEvents->Resized(AvnSize{x,y});
+                if(!_shown)
+                {
+                    BaseEvents->Resized(AvnSize{x,y}, reason);
+                }
+                
+                [Window setContentSize:NSSize{x, y}];
+            }
+            @finally
+            {
+                _inResize = false;
             }
-            
-            [StandardContainer setFrameSize:NSSize{x,y}];
-            [Window setContentSize:NSSize{x, y}];
             
             return S_OK;
         }
@@ -562,6 +578,11 @@ public:
         return S_OK;
     }
 
+    virtual bool IsDialog()
+    {
+        return false;
+    }
+    
 protected:
     virtual NSWindowStyleMask GetStyle()
     {
@@ -592,6 +613,7 @@ private:
     NSRect _preZoomSize;
     bool _transitioningWindowState;
     bool _isClientAreaExtended;
+    bool _isDialog;
     AvnExtendClientAreaChromeHints _extendClientHints;
     
     FORWARD_IUNKNOWN()
@@ -652,13 +674,14 @@ private:
         }
     }
     
-    virtual HRESULT Show (bool activate) override
+    virtual HRESULT Show (bool activate, bool isDialog) override
     {
         START_COM_CALL;
         
         @autoreleasepool
         {
-            WindowBaseImpl::Show(activate);
+            _isDialog = isDialog;
+            WindowBaseImpl::Show(activate, isDialog);
             
             HideOrShowTrafficLights();
             
@@ -690,6 +713,12 @@ private:
             if(cparent == nullptr)
                 return E_INVALIDARG;
             
+            // If one tries to show a child window with a minimized parent window, then the parent window will be
+            // restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive
+            // state. Detect this and explicitly restore the parent window ourselves to avoid this situation.
+            if (cparent->WindowState() == Minimized)
+                cparent->SetWindowState(Normal);
+            
             [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
             
             UpdateStyle();
@@ -757,6 +786,7 @@ private:
                 }
                 
                 _lastWindowState = state;
+                _actualWindowState = state;
                 WindowEvents->WindowStateChanged(state);
             }
         }
@@ -1180,6 +1210,7 @@ private:
                 }
                 
                 _actualWindowState = _lastWindowState;
+                WindowEvents->WindowStateChanged(_actualWindowState);
             }
             
             
@@ -1197,6 +1228,11 @@ private:
         }
     }
     
+    virtual bool IsDialog() override
+    {
+        return _isDialog;
+    }
+    
 protected:
     virtual NSWindowStyleMask GetStyle() override
     {
@@ -1276,6 +1312,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     [_blurBehind setWantsLayer:true];
     _blurBehind.hidden = true;
     
+    [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+    [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+    
     [self addSubview:_blurBehind];
     [self addSubview:_content];
     
@@ -1311,9 +1350,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _settingSize = true;
     [super setFrameSize:newSize];
     
-    [_blurBehind setFrameSize:newSize];
-    [_content setFrameSize:newSize];
-    
     auto window = objc_cast<AvnWindow>([self window]);
     
     // TODO get actual titlebar size
@@ -1329,6 +1365,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     [_titleBarMaterial setFrame:tbar];
     tbar.size.height = height < 1 ? 0 : 1;
     [_titleBarUnderline setFrame:tbar];
+
     _settingSize = false;
 }
 
@@ -1356,6 +1393,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     bool _lastKeyHandled;
     AvnPixelSize _lastPixelSize;
     NSObject<IRenderTarget>* _renderTarget;
+    AvnPlatformResizeReason _resizeReason;
 }
 
 - (void)onClosed
@@ -1467,7 +1505,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
         _lastPixelSize.Height = (int)fsize.height;
         [self updateRenderTarget];
     
-        _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
+        auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
+        _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
     }
 }
 
@@ -1966,6 +2005,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
 }
 
+- (AvnPlatformResizeReason)getResizeReason
+{
+    return _resizeReason;
+}
+
+- (void)setResizeReason:(AvnPlatformResizeReason)reason
+{
+    _resizeReason = reason;
+}
+
 @end
 
 
@@ -1985,6 +2034,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _isExtended = value;
 }
 
+-(bool) isDialog
+{
+    return _parent->IsDialog();
+}
+
 -(double) getScaling
 {
     return _lastScaling;
@@ -2014,18 +2068,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 +(void)closeAll
 {
-    NSArray<NSWindow*>* windows = [NSArray arrayWithArray:[NSApp windows]];
-    auto numWindows = [windows count];
-    
-    for(int i = 0; i < numWindows; i++)
-    {
-        auto window = (AvnWindow*)[windows objectAtIndex:i];
-        
-        if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
-        {
-            [window performClose:nil];
-        }
-    }
+    [[NSApplication sharedApplication] terminate:self];
 }
 
 - (void)performClose:(id)sender
@@ -2174,7 +2217,22 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 -(BOOL)canBecomeKeyWindow
 {
-    return _canBecomeKeyAndMain;
+    if (_canBecomeKeyAndMain)
+    {
+        // If the window has a child window being shown as a dialog then don't allow it to become the key window.
+        for(NSWindow* uch in [self childWindows])
+        {
+            auto ch = objc_cast<AvnWindow>(uch);
+            if(ch == nil)
+                continue;
+            if (ch.isDialog)
+                return false;
+        }
+        
+        return true;
+    }
+    
+    return false;
 }
 
 -(BOOL)canBecomeMainWindow
@@ -2182,22 +2240,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     return _canBecomeKeyAndMain;
 }
 
--(bool) activateAppropriateChild: (bool)activating
-{
-    for(NSWindow* uch in [self childWindows])
-    {
-        auto ch = objc_cast<AvnWindow>(uch);
-        if(ch == nil)
-            continue;
-        [ch activateAppropriateChild:false];
-        return FALSE;
-    }
-    
-    if(!activating)
-        [self makeKeyAndOrderFront:self];
-    return TRUE;
-}
-
 -(bool)shouldTryToHandleEvents
 {
     return _isEnabled;
@@ -2208,26 +2250,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _isEnabled = enable;
 }
 
--(void)makeKeyWindow
-{
-    if([self activateAppropriateChild: true])
-    {
-        [super makeKeyWindow];
-    }
-}
-
 -(void)becomeKeyWindow
 {
     [self showWindowMenuWithAppMenu];
     
-    if([self activateAppropriateChild: true])
+    if(_parent != nullptr)
     {
-        if(_parent != nullptr)
-        {
-            _parent->BaseEvents->Activated();
-        }
+        _parent->BaseEvents->Activated();
     }
-    
+
     [super becomeKeyWindow];
 }
 
@@ -2237,7 +2268,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     if(parent != nil)
     {
         [parent removeChildWindow:self];
-        [parent activateAppropriateChild: false];
     }
 }
 
@@ -2372,13 +2402,14 @@ protected:
         return NSWindowStyleMaskBorderless;
     }
     
-    virtual HRESULT Resize(double x, double y) override
+    virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override
     {
+        START_COM_CALL;
+        
         @autoreleasepool
         {
             if (Window != nullptr)
             {
-                [StandardContainer setFrameSize:NSSize{x,y}];
                 [Window setContentSize:NSSize{x, y}];
             
                 [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];

+ 0 - 102
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@@ -1,102 +0,0 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
-             x:Class="ControlCatalog.Pages.ContextFlyoutPage">
-    <UserControl.Styles>
-        <Style Selector="FlyoutPresenter.NoPadding">
-            <Setter Property="Padding" Value="0" />
-        </Style>
-    </UserControl.Styles>
-    
-    <StackPanel Orientation="Vertical" Spacing="4">
-        <TextBlock Classes="h1">Context Flyout</TextBlock>
-        <TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
-
-        <StackPanel Orientation="Horizontal"
-              Margin="0,16,0,0"
-              HorizontalAlignment="Center"
-              Spacing="16">
-            <Border Background="{DynamicResource SystemAccentColor}"
-                    Margin="16"
-                    Padding="48,48,48,48">
-                <Border.ContextFlyout>
-                    <MenuFlyout>
-                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
-                        <MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
-                        <Separator/>
-                        <MenuItem Header="Menu with _Submenu">
-                            <MenuItem Header="Submenu _1"/>
-                            <MenuItem Header="Submenu _2"/>
-                        </MenuItem>
-                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
-                            <MenuItem.Icon>
-                                <Image Source="/Assets/github_icon.png"/>
-                            </MenuItem.Icon>
-                        </MenuItem>
-                        <MenuItem Header="Menu Item with _Checkbox">
-                            <MenuItem.Icon>
-                                <CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
-                            </MenuItem.Icon>
-                        </MenuItem>
-                    </MenuFlyout>
-                </Border.ContextFlyout>
-                <TextBlock Text="Defined in XAML"/>
-            </Border>
-            <Border Background="{DynamicResource SystemAccentColor}"
-                    Margin="16"
-                    Padding="48,48,48,48">
-                <Border.ContextMenu>
-                    <ContextMenu Items="{Binding MenuItems}">
-                        <ContextMenu.Styles>
-                            <Style Selector="MenuItem">
-                                <Setter Property="Header" Value="{Binding Header}"/>
-                                <Setter Property="Items" Value="{Binding Items}"/>
-                                <Setter Property="Command" Value="{Binding Command}"/>
-                                <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
-                            </Style>
-                        </ContextMenu.Styles>
-                    </ContextMenu>
-                </Border.ContextMenu>
-                <TextBlock Text="Dynamically Generated"/>
-            </Border>
-        </StackPanel>
-
-        <TextBlock Text="Custom ContextFlyout for TextBox" />
-
-        <TextBox Name="TextBox" Width="150" HorizontalAlignment="Center" ContextMenu="{x:Null}">
-            <TextBox.ContextFlyout>
-                <Flyout FlyoutPresenterClasses="NoPadding">
-                    <StackPanel Orientation="Horizontal">
-                        <StackPanel.Styles>
-                            <Style Selector="Button">
-                                <Setter Property="Background" Value="Transparent" />
-                                <Setter Property="Height" Value="40" />
-                                <Setter Property="Width" Value="40" />
-                                <Setter Property="VerticalContentAlignment" Value="Center" />
-                            </Style>
-                            <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
-                                <Setter Property="Background" Value="Transparent" />
-                                <Setter Property="Opacity" Value="0.5" />
-                            </Style>
-                        </StackPanel.Styles>
-                        <Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
-                            <PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
-                        </Button>
-                        <Button Name="CopyButton" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
-                            <PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
-                        </Button>
-                        <Button Name="PasteButton" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
-                            <PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
-                        </Button>
-                        <Button Name="ClearButton" Command="{Binding $parent[TextBox].Clear}">
-                            <PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
-                        </Button>
-                    </StackPanel>
-                </Flyout>
-            </TextBox.ContextFlyout>
-        </TextBox>
-    
-    </StackPanel>
-</UserControl>

+ 0 - 45
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs

@@ -1,45 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using ControlCatalog.ViewModels;
-using Avalonia.Interactivity;
-namespace ControlCatalog.Pages
-{
-    public class ContextFlyoutPage : UserControl
-    {
-        private TextBox _textBox;
-
-        public ContextFlyoutPage()
-        {
-            InitializeComponent();
-
-            var vm = new ContextFlyoutPageViewModel();
-            vm.View = this;
-            DataContext = vm;
-
-            _textBox = this.FindControl<TextBox>("TextBox");
-
-            var cutButton = this.FindControl<Button>("CutButton");
-            cutButton.Click += CloseFlyout;
-
-            var copyButton = this.FindControl<Button>("CopyButton");
-            copyButton.Click += CloseFlyout;
-
-            var pasteButton = this.FindControl<Button>("PasteButton");
-            pasteButton.Click += CloseFlyout;
-
-            var clearButton = this.FindControl<Button>("ClearButton");
-            clearButton.Click += CloseFlyout;
-        }
-
-        private void CloseFlyout(object sender, RoutedEventArgs e)
-        {
-            _textBox.ContextFlyout.Hide();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-    }
-}

+ 157 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml

@@ -0,0 +1,157 @@
+<UserControl x:Class="ControlCatalog.Pages.ContextFlyoutPage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             d:DesignHeight="450"
+             d:DesignWidth="800"
+             mc:Ignorable="d">
+  <UserControl.Styles>
+    <Style Selector="FlyoutPresenter.NoPadding">
+      <Setter Property="Padding" Value="0" />
+    </Style>
+  </UserControl.Styles>
+
+  <StackPanel Orientation="Vertical" Spacing="4">
+    <StackPanel.Styles>
+      <Style Selector="Border.context-target">
+        <Setter Property="Padding" Value="48,20" />
+        <Setter Property="Margin" Value="8" />
+        <Setter Property="Focusable" Value="True" />
+        <Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
+      </Style>
+      <Style Selector="Border.context-target > :is(Control)">
+        <Setter Property="VerticalAlignment" Value="Center" />
+      </Style>
+    </StackPanel.Styles>
+    <TextBlock Classes="h1">Context Flyout</TextBlock>
+    <TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
+
+    <UniformGrid HorizontalAlignment="Center" Rows="2">
+      <Border Classes="context-target">
+        <Border.ContextFlyout>
+          <MenuFlyout>
+            <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
+            <MenuItem Header="_Disabled Menu Item"
+                      InputGesture="Ctrl+D"
+                      IsEnabled="False" />
+            <Separator />
+            <MenuItem Header="Menu with _Submenu">
+              <MenuItem Header="Submenu _1" />
+              <MenuItem Header="Submenu _2" />
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
+              <MenuItem.Icon>
+                <Image Source="/Assets/github_icon.png" />
+              </MenuItem.Icon>
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Checkbox">
+              <MenuItem.Icon>
+                <CheckBox BorderThickness="0"
+                          IsChecked="True"
+                          IsHitTestVisible="False" />
+              </MenuItem.Icon>
+            </MenuItem>
+          </MenuFlyout>
+        </Border.ContextFlyout>
+        <TextBlock Text="Defined in XAML" />
+      </Border>
+      <Border Classes="context-target">
+        <Border.Styles>
+          <Style Selector="MenuFlyoutPresenter MenuItem">
+            <Setter Property="Header" Value="{Binding Header}"/>
+            <Setter Property="Items" Value="{Binding Items}"/>
+            <Setter Property="Command" Value="{Binding Command}"/>
+            <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+          </Style>
+        </Border.Styles>
+        <Border.ContextFlyout>
+          <MenuFlyout Items="{Binding MenuItems}" />
+        </Border.ContextFlyout>
+        <TextBlock Text="Dynamically Generated"/>
+      </Border>
+      <Border x:Name="CustomContextRequestedBorder"
+              Classes="context-target">
+        <Border.ContextFlyout>
+          <Flyout Content="Should never be visible" />
+        </Border.ContextFlyout>
+        <TextBlock Text="Custom ContextRequested handler" TextWrapping="Wrap" />
+      </Border>
+      <Border x:Name="CancellableContextBorder"
+              Classes="context-target">
+        <Border.ContextFlyout>
+          <Flyout>
+            <CheckBox x:Name="CancelCloseCheckBox" Content="Cancel close" />
+          </Flyout>
+        </Border.ContextFlyout>
+        <StackPanel>
+          <TextBlock Text="Cancellable" />
+          <CheckBox x:Name="CancelOpenCheckBox" Content="Cancel open" />
+        </StackPanel>
+      </Border>
+    </UniformGrid>
+
+    <TextBlock Text="Custom ContextFlyout for TextBox" />
+
+    <TextBox Name="TextBox"
+             Width="150"
+             HorizontalAlignment="Center"
+             ContextMenu="{x:Null}">
+      <TextBox.ContextFlyout>
+        <Flyout FlyoutPresenterClasses="NoPadding">
+          <StackPanel>
+          <StackPanel Orientation="Horizontal">
+            <StackPanel.Styles>
+              <Style Selector="Button">
+                <Setter Property="Background" Value="Transparent" />
+                <Setter Property="Height" Value="40" />
+                <Setter Property="Width" Value="40" />
+                <Setter Property="VerticalContentAlignment" Value="Center" />
+              </Style>
+              <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
+                <Setter Property="Background" Value="Transparent" />
+                <Setter Property="Opacity" Value="0.5" />
+              </Style>
+            </StackPanel.Styles>
+            <Button Name="CutButton"
+                    Command="{Binding $parent[TextBox].Cut}"
+                    IsEnabled="{Binding $parent[TextBox].CanCut}">
+              <PathIcon Width="14"
+                        Height="14"
+                        Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
+            </Button>
+            <Button Name="CopyButton"
+                    Command="{Binding $parent[TextBox].Copy}"
+                    IsEnabled="{Binding $parent[TextBox].CanCopy}">
+              <PathIcon Width="14"
+                        Height="14"
+                        Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
+            </Button>
+            <Button Name="PasteButton"
+                    Command="{Binding $parent[TextBox].Paste}"
+                    IsEnabled="{Binding $parent[TextBox].CanPaste}">
+              <PathIcon Width="14"
+                        Height="14"
+                        Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
+            </Button>
+            <Button Name="ClearButton" Command="{Binding $parent[TextBox].Clear}">
+              <PathIcon Width="14"
+                        Height="14"
+                        Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
+            </Button>
+          </StackPanel>
+            <Border Classes="context-target"
+                    Padding="4, 20">
+              <Border.ContextFlyout>
+                <Flyout>
+                  <TextBlock>Hello world</TextBlock>
+                </Flyout>
+              </Border.ContextFlyout>
+              <TextBlock>Inner context flyout</TextBlock>
+            </Border>
+          </StackPanel>
+        </Flyout>
+      </TextBox.ContextFlyout>
+    </TextBox>
+  </StackPanel>
+</UserControl>

+ 91 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@@ -0,0 +1,91 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+using System;
+using System.ComponentModel;
+
+namespace ControlCatalog.Pages
+{
+    public class ContextFlyoutPage : UserControl
+    {
+        private TextBox _textBox;
+
+        public ContextFlyoutPage()
+        {
+            InitializeComponent();
+
+            DataContext = new ContextPageViewModel();
+
+            _textBox = this.FindControl<TextBox>("TextBox");
+
+            var cutButton = this.FindControl<Button>("CutButton");
+            cutButton.Click += CloseFlyout;
+
+            var copyButton = this.FindControl<Button>("CopyButton");
+            copyButton.Click += CloseFlyout;
+
+            var pasteButton = this.FindControl<Button>("PasteButton");
+            pasteButton.Click += CloseFlyout;
+
+            var clearButton = this.FindControl<Button>("ClearButton");
+            clearButton.Click += CloseFlyout;
+
+            var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
+            customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
+
+            var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
+            cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
+            cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
+        }
+
+        private ContextPageViewModel _model;
+        protected override void OnDataContextChanged(EventArgs e)
+        {
+            if (_model != null)
+                _model.View = null;
+            _model = DataContext as ContextPageViewModel;
+            if (_model != null)
+                _model.View = this;
+
+            base.OnDataContextChanged(e);
+        }
+
+        private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
+        {
+            var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelCloseCheckBox");
+            e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+        }
+
+        private void ContextFlyoutPage_Opening(object sender, EventArgs e)
+        {
+            if (e is CancelEventArgs cancelArgs)
+            {
+                var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelOpenCheckBox");
+                cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+            }
+        }
+
+        private void CloseFlyout(object sender, RoutedEventArgs e)
+        {
+            _textBox.ContextFlyout.Hide();
+        }
+
+        public void CustomContextRequested(object sender, ContextRequestedEventArgs e)
+        {
+            var border = (Border)sender;
+            var textBlock = (TextBlock)border.Child;
+
+            textBlock.Text = e.TryGetPosition(border, out var point)
+                ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}"
+                : "Context was requested without pointer";
+            e.Handled = true;
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 85 - 55
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@@ -1,58 +1,88 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="ControlCatalog.Pages.ContextMenuPage">
-    <StackPanel Orientation="Vertical" Spacing="4">
-        <TextBlock Classes="h1">Context Menu</TextBlock>
-        <TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
+<UserControl x:Class="ControlCatalog.Pages.ContextMenuPage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Orientation="Vertical" Spacing="4">
+    <TextBlock Classes="h1">Context Menu</TextBlock>
+    <TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
 
-        <StackPanel Orientation="Horizontal"
-              Margin="0,16,0,0"
-              HorizontalAlignment="Center"
-              Spacing="16">
-            <Border Background="{DynamicResource SystemAccentColor}"
-                    Margin="16"
-                    Padding="48,48,48,48">
-                <Border.ContextMenu>
-                    <ContextMenu>
-                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
-                        <MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
-                        <Separator/>
-                        <MenuItem Header="Menu with _Submenu">
-                            <MenuItem Header="Submenu _1"/>
-                            <MenuItem Header="Submenu _2"/>
-                        </MenuItem>
-                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
-                            <MenuItem.Icon>
-                                <Image Source="/Assets/github_icon.png"/>
-                            </MenuItem.Icon>
-                        </MenuItem>
-                        <MenuItem Header="Menu Item with _Checkbox">
-                            <MenuItem.Icon>
-                                <CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
-                            </MenuItem.Icon>
-                        </MenuItem>
-                        <MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
-                    </ContextMenu>
-                </Border.ContextMenu>
-                <TextBlock Text="Defined in XAML"/>
-            </Border>
-            <Border Background="{DynamicResource SystemAccentColor}"
-                    Margin="16"
-                    Padding="48,48,48,48">
-                <Border.ContextMenu>
-                    <ContextMenu Items="{Binding MenuItems}">
-                        <ContextMenu.Styles>
-                            <Style Selector="MenuItem">
-                                <Setter Property="Header" Value="{Binding Header}"/>
-                                <Setter Property="Items" Value="{Binding Items}"/>
-                                <Setter Property="Command" Value="{Binding Command}"/>
-                                <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
-                            </Style>
-                        </ContextMenu.Styles>
-                    </ContextMenu>
-                </Border.ContextMenu>
-                <TextBlock Text="Dynamically Generated"/>
-            </Border>
+    <UniformGrid HorizontalAlignment="Center" Rows="2">
+      <UniformGrid.Styles>
+        <Style Selector="UniformGrid > Border">
+          <Setter Property="Padding" Value="48,20" />
+          <Setter Property="Margin" Value="8" />
+          <Setter Property="Focusable" Value="True" />
+          <Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
+        </Style>
+        <Style Selector="UniformGrid > Border > :is(Control)">
+          <Setter Property="VerticalAlignment" Value="Center" />
+        </Style>
+      </UniformGrid.Styles>
+      <Border>
+        <Border.ContextMenu>
+          <ContextMenu>
+            <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
+            <MenuItem Header="_Disabled Menu Item"
+                      InputGesture="Ctrl+D"
+                      IsEnabled="False" />
+            <Separator />
+            <MenuItem Header="Menu with _Submenu">
+              <MenuItem Header="Submenu _1" />
+              <MenuItem Header="Submenu _2" />
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
+              <MenuItem.Icon>
+                <Image Source="/Assets/github_icon.png" />
+              </MenuItem.Icon>
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Checkbox">
+              <MenuItem.Icon>
+                <CheckBox BorderThickness="0"
+                          IsChecked="True"
+                          IsHitTestVisible="False" />
+              </MenuItem.Icon>
+            </MenuItem>
+            <MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
+          </ContextMenu>
+        </Border.ContextMenu>
+        <TextBlock Text="Defined in XAML" />
+      </Border>
+      <Border>
+        <Border.Styles>
+          <Style Selector="ContextMenu MenuItem">
+            <Setter Property="Header" Value="{Binding Header}"/>
+            <Setter Property="Items" Value="{Binding Items}"/>
+            <Setter Property="Command" Value="{Binding Command}"/>
+            <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+          </Style>
+        </Border.Styles>
+        <Border.ContextMenu>
+          <ContextMenu Items="{Binding MenuItems}" />
+        </Border.ContextMenu>
+        <TextBlock Text="Dynamically Generated"/>
+      </Border>
+      <Border x:Name="CustomContextRequestedBorder">
+        <Border.ContextMenu>
+          <ContextMenu>
+            <MenuItem Header="Should never be visible" />
+          </ContextMenu>
+        </Border.ContextMenu>
+        <TextBlock Text="Custom ContextRequested handler" TextWrapping="Wrap" />
+      </Border>
+      <Border x:Name="CancellableContextBorder">
+        <Border.ContextMenu>
+          <ContextMenu>
+            <MenuItem>
+              <MenuItem.Header>
+                <CheckBox x:Name="CancelCloseCheckBox" Content="Cancel close" />
+              </MenuItem.Header>
+            </MenuItem>
+          </ContextMenu>
+        </Border.ContextMenu>
+        <StackPanel>
+          <TextBlock Text="Cancellable" />
+          <CheckBox x:Name="CancelOpenCheckBox" Content="Cancel open" />
         </StackPanel>
-    </StackPanel>
+      </Border>
+    </UniformGrid>
+  </StackPanel>
 </UserControl>

+ 39 - 3
samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs

@@ -1,5 +1,8 @@
 using System;
+using System.ComponentModel;
+
 using Avalonia.Controls;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using ControlCatalog.ViewModels;
 
@@ -10,21 +13,54 @@ namespace ControlCatalog.Pages
         public ContextMenuPage()
         {
             this.InitializeComponent();
-            DataContext = new ContextMenuPageViewModel();
+            DataContext = new ContextPageViewModel();
+
+            var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
+            customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
+
+            var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
+            cancellableContextBorder.ContextMenu!.ContextMenuClosing += ContextFlyoutPage_Closing;
+            cancellableContextBorder.ContextMenu!.ContextMenuOpening += ContextFlyoutPage_Opening;
         }
 
-        private ContextMenuPageViewModel _model;
+        private ContextPageViewModel _model;
         protected override void OnDataContextChanged(EventArgs e)
         {
             if (_model != null)
                 _model.View = null;
-            _model  = DataContext as ContextMenuPageViewModel;
+            _model  = DataContext as ContextPageViewModel;
             if (_model != null)
                 _model.View = this;
 
             base.OnDataContextChanged(e);
         }
 
+        private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
+        {
+            var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelCloseCheckBox");
+            e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+        }
+
+        private void ContextFlyoutPage_Opening(object sender, EventArgs e)
+        {
+            if (e is CancelEventArgs cancelArgs)
+            {
+                var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelOpenCheckBox");
+                cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+            }
+        }
+
+        public void CustomContextRequested(object sender, ContextRequestedEventArgs e)
+        {
+            var border = (Border)sender;
+            var textBlock = (TextBlock)border.Child;
+
+            textBlock.Text = e.TryGetPosition(border, out var point)
+                ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}"
+                : "Context was requested without pointer";
+            e.Handled = true;
+        }
+
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);

+ 15 - 10
samples/ControlCatalog/Pages/DataGridPage.xaml

@@ -23,16 +23,21 @@
     </StackPanel>
     <TabControl Grid.Row="2">
       <TabItem Header="DataGrid">
-        <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
-          <DataGrid.Columns>
-            <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
-            <!-- CompiledBinding example of usage. -->
-            <DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
-            <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
-            <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
-            <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" />
-          </DataGrid.Columns>
-        </DataGrid>
+        <DockPanel>
+          <CheckBox x:Name="ShowGDP"  IsChecked="True"  Content="Toggle GDP Column Visibility"
+                    DockPanel.Dock="Top"/>
+          <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
+            <DataGrid.Columns>
+              <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" MinWidth="400"  />
+              <!-- CompiledBinding example of usage. -->
+              <DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
+              <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
+              <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
+              <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp"
+                                  IsVisible="{Binding #ShowGDP.IsChecked}"/>
+            </DataGrid.Columns>
+          </DataGrid>
+        </DockPanel>
       </TabItem>
       <TabItem Header="Grouping">
         <DataGrid Name="dataGridGrouping" Margin="12">

+ 0 - 1
samples/ControlCatalog/SideBar.xaml

@@ -16,7 +16,6 @@
         <Setter Property="Template">
             <ControlTemplate>
                 <Border 
-                    Margin="{TemplateBinding Margin}"
                     BorderBrush="{TemplateBinding BorderBrush}"
                     BorderThickness="{TemplateBinding BorderThickness}">
                     <DockPanel>

+ 0 - 78
samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs

@@ -1,78 +0,0 @@
-using System.Collections.Generic;
-using System.Reactive;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.VisualTree;
-using MiniMvvm;
-
-namespace ControlCatalog.ViewModels
-{
-    public class ContextFlyoutPageViewModel
-    {
-        public Control View { get; set; }
-        public ContextFlyoutPageViewModel()
-        {
-            OpenCommand = MiniCommand.CreateFromTask(Open);
-            SaveCommand = MiniCommand.Create(Save);
-            OpenRecentCommand = MiniCommand.Create<string>(OpenRecent);
-
-            MenuItems = new[]
-            {
-                new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
-                new MenuItemViewModel { Header = "Save", Command = SaveCommand },
-                new MenuItemViewModel { Header = "-" },
-                new MenuItemViewModel
-                {
-                    Header = "Recent",
-                    Items = new[]
-                    {
-                        new MenuItemViewModel
-                        {
-                            Header = "File1.txt",
-                            Command = OpenRecentCommand,
-                            CommandParameter = @"c:\foo\File1.txt"
-                        },
-                        new MenuItemViewModel
-                        {
-                            Header = "File2.txt",
-                            Command = OpenRecentCommand,
-                            CommandParameter = @"c:\foo\File2.txt"
-                        },
-                    }
-                },
-            };
-        }
-
-        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
-        public MiniCommand OpenCommand { get; }
-        public MiniCommand SaveCommand { get; }
-        public MiniCommand OpenRecentCommand { get; }
-
-        public async Task Open()
-        {
-            var window = View?.GetVisualRoot() as Window;
-            if (window == null)
-                return;
-            var dialog = new OpenFileDialog();
-            var result = await dialog.ShowAsync(window);
-
-            if (result != null)
-            {
-                foreach (var path in result)
-                {
-                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
-                }
-            }
-        }
-
-        public void Save()
-        {
-            System.Diagnostics.Debug.WriteLine("Save");
-        }
-
-        public void OpenRecent(string path)
-        {
-            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
-        }
-    }
-}

+ 2 - 2
samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs → samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@@ -7,10 +7,10 @@ using MiniMvvm;
 
 namespace ControlCatalog.ViewModels
 {
-    public class ContextMenuPageViewModel
+    public class ContextPageViewModel
     {
         public Control View { get; set; }
-        public ContextMenuPageViewModel()
+        public ContextPageViewModel()
         {
             OpenCommand = MiniCommand.CreateFromTask(Open);
             SaveCommand = MiniCommand.Create(Save);

+ 149 - 0
samples/RenderDemo/Pages/AnimationsPage.xaml

@@ -161,6 +161,151 @@
           </Animation>
         </Style.Animations>
       </Style>
+
+      <Style Selector="Border.Rect7">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background" Value="Red" />
+            </KeyFrame>
+            <KeyFrame Cue="30%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="60%">
+              <Setter Property="Background" Value="Blue" />
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Green"/>
+                  <GradientStop Offset="1" Color="Yellow"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect8">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="50%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="0.25" Color="Blue"/>
+                  <GradientStop Offset="0.5" Color="Blue"/>
+                  <GradientStop Offset="0.75" Color="Green"/>
+                  <GradientStop Offset="1" Color="Yellow"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect9">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <ConicGradientBrush Center="50%,50%" Angle="0">
+                  <GradientStop Offset="0" Color="Blue"/>
+                  <GradientStop Offset="0.5" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </ConicGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <ConicGradientBrush Center="50%,70%" Angle="90">
+                  <GradientStop Offset="0" Color="Green"/>
+                  <GradientStop Offset="0.25" Color="Yellow"/>
+                  <GradientStop Offset="0.5" Color="Red"/>
+                  <GradientStop Offset="0.75" Color="Blue"/>
+                  <GradientStop Offset="1" Color="Green"/>
+                </ConicGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect10">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Normal">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,100%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="25%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,0%" Radius="1">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="50%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="100%,0%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="75%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="100%,100%" Radius="1">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,100%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
     </Styles>
   </UserControl.Styles>
   <Grid>
@@ -181,6 +326,10 @@
         <Border Classes="Test Rect6" Background="Red"/>
         <Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
         <Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
+        <Border Classes="Test Rect7" Child="{x:Null}" />
+        <Border Classes="Test Rect8" Child="{x:Null}" />
+        <Border Classes="Test Rect9" Child="{x:Null}" />
+        <Border Classes="Test Rect10" Child="{x:Null}" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 80 - 4
samples/RenderDemo/Pages/TransitionsPage.xaml

@@ -167,13 +167,80 @@
       <Style Selector="Border.Rect11:pointerover">
         <Setter Property="Background" >
           <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
-            <LinearGradientBrush.GradientStops>
-              <GradientStop Offset="0" Color="Red"/>
-              <GradientStop Offset="1" Color="Blue"/>
-            </LinearGradientBrush.GradientStops>
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
           </LinearGradientBrush>
         </Setter>
       </Style>
+
+      <Style Selector="Border.Rect12">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </LinearGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect12:pointerover">
+        <Setter Property="Background" >
+          <LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </LinearGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect13">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <ConicGradientBrush Center="50%,50%" Angle="0">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </ConicGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect13:pointerover">
+        <Setter Property="Background" >
+          <ConicGradientBrush Center="70%,70%" Angle="90">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </ConicGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect14">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <RadialGradientBrush Center="50%,50%" Radius="0.5">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </RadialGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect14:pointerover">
+        <Setter Property="Background" >
+          <RadialGradientBrush Center="30%,30%" Radius="0.2">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </RadialGradientBrush>
+        </Setter>
+      </Style>
     </Styles>
   </UserControl.Styles>
 
@@ -202,6 +269,15 @@
 
         <Border Classes="Test Rect10" />
         <Border Classes="Test Rect11" />
+
+        <Border Classes="Test Rect12" Child="{x:Null}"/>
+        <Border Classes="Test Rect13" Child="{x:Null}"/>
+        <Border Classes="Test Rect14" Child="{x:Null}"/>
+        
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 0 - 1
samples/RenderDemo/SideBar.xaml

@@ -7,7 +7,6 @@
     <Setter Property="Template">
       <ControlTemplate>
         <Border
-            Margin="{TemplateBinding Margin}"
             BorderBrush="{TemplateBinding BorderBrush}"
             BorderThickness="{TemplateBinding BorderThickness}">
           <DockPanel>

+ 2 - 2
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public Action<Rect> Paint { get; set; }
 
-        public Action<Size> Resized { get; set; }
+        public Action<Size, PlatformResizeReason> Resized { get; set; }
 
         public Action<double> ScalingChanged { get; set; }
 
@@ -134,7 +134,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         protected virtual void OnResized(Size size)
         {
-            Resized?.Invoke(size);
+            Resized?.Invoke(size, PlatformResizeReason.Unspecified);
         }
 
         class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo

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

@@ -861,7 +861,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Logs a mesage if the notification represents a binding error.
+        /// Logs a message if the notification represents a binding error.
         /// </summary>
         /// <param name="property">The property being bound.</param>
         /// <param name="value">The binding notification.</param>

+ 2 - 2
src/Avalonia.Base/AvaloniaProperty.cs

@@ -465,9 +465,9 @@ namespace Avalonia
         /// Uses the visitor pattern to resolve an untyped property to a typed property.
         /// </summary>
         /// <typeparam name="TData">The type of user data passed.</typeparam>
-        /// <param name="vistor">The visitor which will accept the typed property.</param>
+        /// <param name="visitor">The visitor which will accept the typed property.</param>
         /// <param name="data">The user data to pass.</param>
-        public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
+        public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
             where TData : struct;
 
         /// <summary>

+ 2 - 2
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@@ -58,8 +58,8 @@ namespace Avalonia
         /// <remarks>
         /// This will usually be true, except in
         /// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/>
-        /// which recieves notifications for all changes to property values, whether a value with a higher
-        /// priority is present or not. When this property is false, the change that is being signalled
+        /// which receives notifications for all changes to property values, whether a value with a higher
+        /// priority is present or not. When this property is false, the change that is being signaled
         /// has not resulted in a change to the property value on the object.
         /// </remarks>
         public bool IsEffectiveValueChange { get; private set; }

+ 1 - 1
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@@ -1271,7 +1271,7 @@ namespace Avalonia.Collections.Pooled
         /// Reverses the elements in a range of this list. Following a call to this
         /// method, an element in the range given by index and count
         /// which was previously located at index i will now be located at
-        /// index index + (index + count - i - 1).
+        /// index + (index + count - i - 1).
         /// </summary>
         public void Reverse(int index, int count)
         {

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

@@ -18,5 +18,11 @@ namespace Avalonia.Data.Converters
         /// </summary>
         public static readonly IMultiValueConverter Or =
             new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y));
+
+        /// <summary>
+        /// A value converter that returns true when input is false and false when input is true.
+        /// </summary>
+        public static readonly IValueConverter Not =
+            new FuncValueConverter<bool, bool>(x => !x);
     }
 }

+ 1 - 1
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins
         /// </summary>
         /// <param name="reference">A weak reference to the object.</param>
         /// <param name="propertyName">The property name.</param>
-        /// <param name="inner">The inner property accessor used to aceess the property.</param>
+        /// <param name="inner">The inner property accessor used to access the property.</param>
         /// <returns>
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.

+ 3 - 3
src/Avalonia.Base/DirectPropertyBase.cs

@@ -13,7 +13,7 @@ namespace Avalonia
     /// <typeparam name="TValue">The type of the property's value.</typeparam>
     /// <remarks>
     /// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
-    /// class provides a non-owner-typed interface to a direct poperty.
+    /// class provides a non-owner-typed interface to a direct property.
     /// </remarks>
     public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue>
     {
@@ -123,9 +123,9 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
+        public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
         {
-            vistor.Visit(this, ref data);
+            visitor.Visit(this, ref data);
         }
 
         /// <inheritdoc/>

+ 1 - 1
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -38,7 +38,7 @@ namespace Avalonia
         /// <remarks>
         /// Data validation is validation performed at the target of a binding, for example in a
         /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
-        /// control (such as a TextBox's Text property) will be interested in recieving data
+        /// control (such as a TextBox's Text property) will be interested in receiving data
         /// validation messages so this feature must be explicitly enabled by setting this flag.
         /// </remarks>
         public bool? EnableDataValidation { get; private set; }

+ 4 - 4
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -877,7 +877,7 @@ namespace Avalonia.Collections
                         if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred))
                         {
                             // if the temporaryGroup was not created yet and is out of sync
-                            // then create it so that we can use it as a refernce while paging.
+                            // then create it so that we can use it as a reference while paging.
                             if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count)
                             {
                                 PrepareTemporaryGroups();
@@ -889,7 +889,7 @@ namespace Avalonia.Collections
                     else if (IsGrouping)
                     {
                         // if the temporaryGroup was not created yet and is out of sync
-                        // then create it so that we can use it as a refernce while paging.
+                        // then create it so that we can use it as a reference while paging.
                         if (_temporaryGroup.ItemCount != InternalList.Count)
                         {
                             // update the groups that get created for the
@@ -1951,7 +1951,7 @@ namespace Avalonia.Collections
             EnsureCollectionInSync();
             VerifyRefreshNotDeferred();
 
-            // for indicies larger than the count
+            // for indices larger than the count
             if (index >= Count || index < 0)
             {
                 throw new ArgumentOutOfRangeException("index");
@@ -3800,7 +3800,7 @@ namespace Avalonia.Collections
         /// </summary>
         /// <remarks>
         /// This method can be called from a constructor - it does not call
-        /// any virtuals.  The 'count' parameter is substitute for the real Count,
+        /// any virtuals. The 'count' parameter is substitute for the real Count,
         /// used only when newItem is null.
         /// In that case, this method sets IsCurrentAfterLast to true if and only
         /// if newPosition >= count.  This distinguishes between a null belonging

+ 50 - 10
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -25,6 +25,7 @@ using System.ComponentModel.DataAnnotations;
 using Avalonia.Controls.Utils;
 using Avalonia.Layout;
 using Avalonia.Controls.Metadata;
+using Avalonia.Input.GestureRecognizers;
 
 namespace Avalonia.Controls
 {
@@ -67,7 +68,7 @@ namespace Avalonia.Controls
         private const double DATAGRID_minimumColumnHeaderHeight = 4;
         internal const double DATAGRID_maximumStarColumnWidth = 10000;
         internal const double DATAGRID_minimumStarColumnWidth = 0.001;
-        private const double DATAGRID_mouseWheelDelta = 72.0;
+        private const double DATAGRID_mouseWheelDelta = 50.0;
         private const double DATAGRID_maxHeadersThickness = 32768;
 
         private const double DATAGRID_defaultRowHeight = 22;
@@ -2214,32 +2215,71 @@ namespace Avalonia.Controls
         /// <param name="e">PointerWheelEventArgs</param>
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         {
-            if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0)
+            e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta);
+        }
+
+        internal bool UpdateScroll(Vector delta)
+        {
+            if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
             {
-                double scrollHeight = 0;
-                if (e.Delta.Y > 0)
+                var handled = false;
+                var scrollHeight = 0d;
+
+                // Vertical scroll handling
+                if (delta.Y > 0)
                 {
-                    scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta);
+                    scrollHeight = Math.Max(-_verticalOffset, -delta.Y);
                 }
-                else if (e.Delta.Y < 0)
+                else if (delta.Y < 0)
                 {
                     if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible)
                     {
-                        scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+                        scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y);
                     }
                     else
                     {
                         double maximum = EdgedRowsHeightCalculated - CellsHeight;
-                        scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+                        scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y);
                     }
                 }
+
                 if (scrollHeight != 0)
                 {
                     DisplayData.PendingVerticalScrollHeight = scrollHeight;
+                    handled = true;
+                }
+
+                // Horizontal scroll handling
+                if (delta.X != 0)
+                {
+                    var originalHorizontalOffset = HorizontalOffset;
+                    var horizontalOffset = originalHorizontalOffset - delta.X;
+                    var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
+
+                    if (horizontalOffset < 0)
+                    {
+                        horizontalOffset = 0;
+                    }
+                    if (horizontalOffset > widthNotVisible)
+                    {
+                        horizontalOffset = widthNotVisible;
+                    }
+
+                    if (horizontalOffset != originalHorizontalOffset)
+                    {
+                        HorizontalOffset = horizontalOffset;
+                        handled = true;
+                    }
+                }
+
+                if (handled)
+                {
                     InvalidateRowsMeasure(invalidateIndividualElements: false);
-                    e.Handled = true;
+                    return true;
                 }
             }
+
+            return false;
         }
 
         /// <summary>
@@ -5731,7 +5771,7 @@ namespace Avalonia.Controls
                 {
                     if (SelectionMode == DataGridSelectionMode.Single || !ctrl)
                     {
-                        // Unselect the currectly selected rows except the new selected row
+                        // Unselect the currently selected rows except the new selected row
                         action = DataGridSelectionAction.SelectCurrent;
                     }
                     else

+ 10 - 2
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@@ -173,7 +173,15 @@ namespace Avalonia.Controls
                     }
                     if (OwningRow != null)
                     {
-                        e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
+                        var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
+
+                        // Do not handle PointerPressed with touch,
+                        // so we can start scroll gesture on the same event.
+                        if (e.Pointer.Type != PointerType.Touch)
+                        {
+                            e.Handled = handled;
+                        }
+
                         OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
                     }
                 }
@@ -197,7 +205,7 @@ namespace Avalonia.Controls
         }
 
         // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the 
-        // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column
+        // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column
         internal void EnsureGridLine(DataGridColumn lastVisibleColumn)
         {
             if (OwningGrid != null && _rightGridLine != null)

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs

@@ -40,7 +40,7 @@ namespace Avalonia.Controls
             return false;
         }
 
-        // There is build warning if this is missiing
+        // There is build warning if this is missing
         public override int GetHashCode()
         {
             return base.GetHashCode();

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGridClipboard.cs

@@ -189,7 +189,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// DataGrid row item used for proparing the ClipboardRowContent.
+        /// DataGrid row item used for preparing the ClipboardRowContent.
         /// </summary>
         public object Item
         {

+ 26 - 18
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -27,7 +27,6 @@ namespace Avalonia.Controls
         private double? _minWidth;
         private bool _settingWidthInternally;
         private int _displayIndexWithFiller;
-        private bool _isVisible;
         private object _header;
         private DataGridColumnHeader _headerCell;
         private IControl _editingElement;
@@ -40,7 +39,6 @@ namespace Avalonia.Controls
         /// </summary>
         protected internal DataGridColumn()
         {
-            _isVisible = true;
             _displayIndexWithFiller = -1;
             IsInitialDesiredWidthDetermined = false;
             InheritsWidth = true;
@@ -174,32 +172,42 @@ namespace Avalonia.Controls
             get => _editBinding;
         }
 
+
+        /// <summary>
+        /// Defines the <see cref="IsVisible"/> property.
+        /// </summary>
+        public static StyledProperty<bool> IsVisibleProperty =
+             Control.IsVisibleProperty.AddOwner<DataGridColumn>();
+
         /// <summary>
         /// Determines whether or not this column is visible.
         /// </summary>
         public bool IsVisible
         {
-            get
-            {
-                return _isVisible;
-            }
-            set
-            {
-                if (value != IsVisible)
-                {
-                    OwningGrid?.OnColumnVisibleStateChanging(this);
-                    _isVisible = value;
+            get => GetValue(IsVisibleProperty);
+            set => SetValue(IsVisibleProperty, value);
+        }
 
-                    if (_headerCell != null)
-                    {
-                        _headerCell.IsVisible = value;
-                    }
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
 
-                    OwningGrid?.OnColumnVisibleStateChanged(this);
+            if (change.Property == IsVisibleProperty)
+            {
+                OwningGrid?.OnColumnVisibleStateChanging(this);
+                var isVisible = (change as AvaloniaPropertyChangedEventArgs<bool>).NewValue.Value;
+
+                if (_headerCell != null)
+                {
+                    _headerCell.IsVisible = isVisible;
                 }
+
+                OwningGrid?.OnColumnVisibleStateChanged(this);
+                NotifyPropertyChanged(change.Property.Name);
             }
         }
 
+
         /// <summary>
         /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level
         /// have been taken into account
@@ -787,7 +795,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to
+        /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to
         /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal
         /// rounding process, so we need to do it ourselves.  If we don't, then we'll end up with some
         /// pixel gaps and/or overlaps between columns.

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs

@@ -79,7 +79,7 @@ namespace Avalonia.Controls
             set;
         }
 
-        internal void AddRecylableRow(DataGridRow row)
+        internal void AddRecyclableRow(DataGridRow row)
         {
             Debug.Assert(!_recyclableRows.Contains(row));
             row.DetachFromDataGrid(true);
@@ -120,7 +120,7 @@ namespace Avalonia.Controls
                     {
                         if (row.IsRecyclable)
                         {
-                            AddRecylableRow(row);
+                            AddRecyclableRow(row);
                         }
                         else
                         {
@@ -193,7 +193,7 @@ namespace Avalonia.Controls
 
         internal void FullyRecycleElements()
         {
-            // Fully recycle Recycleable rows and transfer them to Recycled rows
+            // Fully recycle Recyclable rows and transfer them to Recycled rows
             while (_recyclableRows.Count > 0)
             {
                 DataGridRow row = _recyclableRows.Pop();
@@ -202,7 +202,7 @@ namespace Avalonia.Controls
                 Debug.Assert(!_fullyRecycledRows.Contains(row));
                 _fullyRecycledRows.Push(row);
             }
-            // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders
+            // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders
             while (_recyclableGroupHeaders.Count > 0)
             {
                 DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop();

+ 3 - 3
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@@ -392,7 +392,7 @@ namespace Avalonia.Controls
             set;
         }
 
-        // Height that the row will eventually end up at after a possible detalis animation has completed
+        // Height that the row will eventually end up at after a possible details animation has completed
         internal double TargetHeight
         {
             get
@@ -517,7 +517,7 @@ namespace Avalonia.Controls
                 return base.MeasureOverride(availableSize);
             }
 
-            //Allow the DataGrid specific componets to adjust themselves based on new values
+            //Allow the DataGrid specific components to adjust themselves based on new values
             if (_headerElement != null)
             {
                 _headerElement.InvalidateMeasure();
@@ -722,7 +722,7 @@ namespace Avalonia.Controls
                 if (_bottomGridLine != null)
                 {
                     // It looks like setting Visibility sometimes has side effects so make sure the value is actually
-                    // diffferent before setting it
+                    // different before setting it
                     bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All;
 
                     if (newVisibility != _bottomGridLine.IsVisible)

+ 9 - 9
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@@ -1193,7 +1193,7 @@ namespace Avalonia.Controls
                 else
                 {
                     groupHeader = element as DataGridRowGroupHeader;
-                    Debug.Assert(groupHeader != null);  // Nothig other and Rows and RowGroups now
+                    Debug.Assert(groupHeader != null);  // Nothing other and Rows and RowGroups now
                     if (groupHeader != null)
                     {
                         groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1];
@@ -1636,7 +1636,7 @@ namespace Avalonia.Controls
             if (slot >= DisplayData.FirstScrollingSlot &&
                 slot <= DisplayData.LastScrollingSlot)
             {
-                // Additional row takes the spot of a displayed row - it is necessarilly displayed
+                // Additional row takes the spot of a displayed row - it is necessarily displayed
                 return true;
             }
             else if (DisplayData.FirstScrollingSlot == -1 &&
@@ -1825,7 +1825,7 @@ namespace Avalonia.Controls
                 if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
                 {
                     // We've scrolled off more of the first row than what's possible.  This can happen
-                    // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
+                    // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling
                     // cleanup issue.  In this case, simply try to display the next row as the first row instead
                     if (newFirstScrollingSlot < SlotCount - 1)
                     {
@@ -2014,7 +2014,7 @@ namespace Avalonia.Controls
 
             if (recycleRow)
             {
-                DisplayData.AddRecylableRow(dataGridRow);
+                DisplayData.AddRecyclableRow(dataGridRow);
             }
             else
             {
@@ -2265,7 +2265,7 @@ namespace Avalonia.Controls
                     if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1)
                     {
                         // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed
-                        EnsureAnscestorsExpanderButtonChecked(parentGroupInfo);
+                        EnsureAncestorsExpanderButtonChecked(parentGroupInfo);
                     }
                 }
             }
@@ -2407,7 +2407,7 @@ namespace Avalonia.Controls
             return treeCount;
         }
 
-        private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
+        private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
         {
             if (IsSlotVisible(parentGroupInfo.Slot))
             {
@@ -2789,11 +2789,11 @@ namespace Avalonia.Controls
             return null;
         }
 
-        internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent)
+        internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent)
         {
             Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0);
 
-            if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit())
+            if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit())
             {
                 return;
             }
@@ -2804,7 +2804,7 @@ namespace Avalonia.Controls
                 UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
             }
 
-            UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true);
+            UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true);
 
             ComputeScrollBarsLayout();
             // We need force arrange since our Scrollings Rows could update without automatically triggering layout

+ 1 - 1
src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs

@@ -140,7 +140,7 @@ namespace Avalonia.Controls.Primitives
                 if (dataGridColumn.IsFrozen)
                 {
                     columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
-                    columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform
+                    columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform
                     if (DragColumn == dataGridColumn && DragIndicator != null)
                     {
                         dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset;

+ 13 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs

@@ -5,6 +5,9 @@
 
 using System;
 using System.Diagnostics;
+
+using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
 using Avalonia.Layout;
 using Avalonia.Media;
 
@@ -16,6 +19,11 @@ namespace Avalonia.Controls.Primitives
     /// </summary>
     public sealed class DataGridRowsPresenter : Panel
     {
+        public DataGridRowsPresenter()
+        {
+            AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
+        }
+
         internal DataGrid OwningGrid
         {
             get;
@@ -176,6 +184,11 @@ namespace Avalonia.Controls.Primitives
             return new Size(totalCellsWidth + headerWidth, totalHeight);
         }
 
+        private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
+        {
+            e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta);
+        }
+
 #if DEBUG
         internal void PrintChildren()
         {

+ 5 - 1
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -247,7 +247,11 @@
             <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
             <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
 
-            <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
+            <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1">
+              <DataGridRowsPresenter.GestureRecognizers>
+                <ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
+              </DataGridRowsPresenter.GestureRecognizers>
+            </DataGridRowsPresenter>
             <Rectangle Name="PART_BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
             <Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
             <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>

+ 7 - 3
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@@ -6,8 +6,8 @@
     <x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
     <x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
 
-    <StreamGeometry x:Key="DataGridSortIconAscendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
-    <StreamGeometry x:Key="DataGridSortIconDescendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
+    <StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
+    <StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
     <StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
     <StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>
 
@@ -601,7 +601,11 @@
             <DataGridRowsPresenter Name="PART_RowsPresenter"
                                    Grid.Row="1"
                                    Grid.RowSpan="2"
-                                   Grid.ColumnSpan="3" />
+                                   Grid.ColumnSpan="3">
+              <DataGridRowsPresenter.GestureRecognizers>
+                <ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
+              </DataGridRowsPresenter.GestureRecognizers>
+            </DataGridRowsPresenter>
             <Rectangle Name="PART_BottomRightCorner"
                        Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
                        Grid.Column="2"

+ 22 - 2
src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs

@@ -340,10 +340,30 @@ namespace Avalonia.Controls.Utils
         internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index)
         {
             index = null;
+            // Return the default value of GetProperty if the first character is not an indexer token.
             if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken)
             {
-                // Return the default value of GetProperty if the first character is not an indexer token.
-                return type.GetProperty(propertyPath);
+                var property = type.GetProperty(propertyPath);
+                if (property != null)
+                {
+                    return property;
+                }
+
+                // GetProperty does not return inherited interface properties,
+                // so we need to enumerate them manually.
+                if (type.IsInterface)
+                {
+                    foreach (var typeInterface in type.GetInterfaces())
+                    {
+                        property = type.GetProperty(propertyPath);
+                        if (property != null)
+                        {
+                            return property;
+                        }
+                    }
+                }
+
+                return null;
             }
 
             if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken)

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

@@ -30,12 +30,29 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV
 MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
+MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist 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 System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Action<Avalonia.Size, Avalonia.Platform.PlatformResizeReason> Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Action<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size, Avalonia.Platform.PlatformResizeReason>)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size>)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist 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: 39
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
+Total Issues: 56

+ 1 - 1
src/Avalonia.Controls/AppBuilderBase.cs

@@ -273,7 +273,7 @@ namespace Avalonia.Controls
         }
         
         /// <summary>
-        /// Sets up the platform-speciic services for the <see cref="Application"/>.
+        /// Sets up the platform-specific services for the <see cref="Application"/>.
         /// </summary>
         private void Setup()
         {

+ 28 - 1
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using System.Threading;
 using Avalonia.Controls;
@@ -42,9 +43,13 @@ namespace Avalonia.Controls.ApplicationLifetimes
                     "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
             _activeLifetime = this;
         }
-        
+
         /// <inheritdoc/>
         public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup;
+
+        /// <inheritdoc/>
+        public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
+
         /// <inheritdoc/>
         public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;
 
@@ -111,6 +116,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
                 ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
             }
 
+            var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>(); 
+
+            if (lifetimeEvents != null)
+                lifetimeEvents.ShutdownRequested += OnShutdownRequested;
+
             _cts = new CancellationTokenSource();
             MainWindow?.Show();
             Dispatcher.UIThread.MainLoop(_cts.Token);
@@ -123,6 +133,23 @@ namespace Avalonia.Controls.ApplicationLifetimes
             if (_activeLifetime == this)
                 _activeLifetime = null;
         }
+        
+        private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)
+        {
+            ShutdownRequested?.Invoke(this, e);
+
+            if (e.Cancel)
+                return;
+
+            // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
+            // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
+            // owners.
+            foreach (var w in Windows)
+                if (w.Owner is null)
+                    w.Close();
+            if (Windows.Count > 0)
+                e.Cancel = true;
+        }
     }
     
     public class ClassicDesktopStyleApplicationLifetimeOptions

+ 19 - 0
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 
 namespace Avalonia.Controls.ApplicationLifetimes
 {
@@ -34,5 +35,23 @@ namespace Avalonia.Controls.ApplicationLifetimes
         Window MainWindow { get; set; }
         
         IReadOnlyList<Window> Windows { get; }
+
+        /// <summary>
+        /// Raised by the platform when an application shutdown is requested.
+        /// </summary>
+        /// <remarks>
+        /// Application Shutdown can be requested for various reasons like OS shutdown.
+        /// 
+        /// On Windows this will be called when an OS Session (logout or shutdown) terminates. Cancelling the eventargs will 
+        /// block OS shutdown.
+        /// 
+        /// On OSX this has the same behavior as on Windows and in addition:
+        /// This event is raised via the Quit menu or right-clicking on the application icon and selecting Quit. 
+        /// 
+        /// This event provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application
+        /// will try to close each non-owned open window, invoking the <see cref="Window.Closing"/> event on each and allowing
+        /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown.
+        /// </remarks>
+        event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
     }
 }

+ 9 - 0
src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs

@@ -0,0 +1,9 @@
+using System.ComponentModel;
+
+namespace Avalonia.Controls.ApplicationLifetimes
+{
+    public class ShutdownRequestedEventArgs : CancelEventArgs
+    {
+
+    }
+}

+ 2 - 2
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -2005,7 +2005,7 @@ namespace Avalonia.Controls
             // The TextBox.TextChanged event was not firing immediately and
             // was causing an immediate update, even with wrapping. If there is
             // a selection currently, no update should happen.
-            if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length)
+            if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != (TextBox.Text?.Length ?? 0))
             {
                 return;
             }
@@ -2303,7 +2303,7 @@ namespace Avalonia.Controls
             {
                 if (IsTextCompletionEnabled && TextBox != null && userInitiated)
                 {
-                    int currentLength = TextBox.Text.Length;
+                    int currentLength = TextBox.Text?.Length ?? 0;
                     int selectionStart = TextBoxSelectionStart;
                     if (selectionStart == text.Length && selectionStart > _textSelectionStart)
                     {

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

@@ -91,6 +91,7 @@ namespace Avalonia.Controls
             FocusableProperty.OverrideDefaultValue<ComboBox>(true);
             SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x,e) => x.SelectedItemChanged(e));
             KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
+            IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
         }
 
         /// <summary>

+ 61 - 19
src/Avalonia.Controls/ContextMenu.cs

@@ -1,12 +1,15 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Diagnostics;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Styling;
@@ -18,7 +21,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control context menu.
     /// </summary>
-    public class ContextMenu : MenuBase, ISetterValue
+    public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
     {
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
@@ -79,6 +82,7 @@ namespace Avalonia.Controls
         private Popup? _popup;
         private List<Control>? _attachedControls;
         private IInputElement? _previousFocus;
+        private Action<IPopupHost?>? _popupHostChangedHandler;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ContextMenu"/> class.
@@ -220,7 +224,8 @@ namespace Avalonia.Controls
 
             if (e.OldValue is ContextMenu oldMenu)
             {
-                control.PointerReleased -= ControlPointerReleased;
+                control.ContextRequested -= ControlContextRequested;
+                control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
                 oldMenu._attachedControls?.Remove(control);
                 ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
             }
@@ -229,7 +234,8 @@ namespace Avalonia.Controls
             {
                 newMenu._attachedControls ??= new List<Control>();
                 newMenu._attachedControls.Add(control);
-                control.PointerReleased += ControlPointerReleased;
+                control.ContextRequested += ControlContextRequested;
+                control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
             }
         }
 
@@ -269,7 +275,7 @@ namespace Avalonia.Controls
             }
 
             control ??= _attachedControls![0];
-            Open(control, PlacementTarget ?? control);
+            Open(control, PlacementTarget ?? control, false);
         }
 
         /// <summary>
@@ -299,12 +305,20 @@ namespace Avalonia.Controls
             }
         }
 
+        IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
+
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged 
+        { 
+            add => _popupHostChangedHandler += value; 
+            remove => _popupHostChangedHandler -= value;
+        }
+
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
             return new MenuItemContainerGenerator(this);
         }
 
-        private void Open(Control control, Control placementTarget)
+        private void Open(Control control, Control placementTarget, bool requestedByPointer)
         {
             if (IsOpen)
             {
@@ -329,6 +343,8 @@ namespace Avalonia.Controls
 
                 _popup.Opened += PopupOpened;
                 _popup.Closed += PopupClosed;
+                _popup.Closing += PopupClosing;
+                _popup.KeyUp += PopupKeyUp;
             }
 
             if (_popup.Parent != control)
@@ -337,6 +353,10 @@ namespace Avalonia.Controls
                 ((ISetLogicalParent)_popup).SetParent(control);
             }
 
+            _popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer
+                ? PlacementMode.Bottom
+                : PlacementMode;
+
             _popup.PlacementTarget = placementTarget;
             _popup.Child = this;
             IsOpen = true;
@@ -353,6 +373,13 @@ namespace Avalonia.Controls
         {
             _previousFocus = FocusManager.Instance?.Current;
             Focus();
+
+            _popupHostChangedHandler?.Invoke(_popup!.Host);
+        }
+
+        private void PopupClosing(object sender, CancelEventArgs e)
+        {
+            e.Cancel = CancelClosing();
         }
 
         private void PopupClosed(object sender, EventArgs e)
@@ -381,32 +408,47 @@ namespace Avalonia.Controls
                 RoutedEvent = MenuClosedEvent,
                 Source = this,
             });
+            
+            _popupHostChangedHandler?.Invoke(null);
         }
 
-        private static void ControlPointerReleased(object sender, PointerReleasedEventArgs e)
+        private void PopupKeyUp(object sender, KeyEventArgs e)
         {
-            var control = (Control)sender;
-            var contextMenu = control.ContextMenu;
-
-            if (control.ContextMenu.IsOpen)
+            if (IsOpen)
             {
-                if (contextMenu.CancelClosing())
-                    return;
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
 
-                control.ContextMenu.Close();
-                e.Handled = true;
+                if (keymap.OpenContextMenu.Any(k => k.Matches(e))
+                    && !CancelClosing())
+                {
+                    Close();
+                    e.Handled = true;
+                }
             }
+        }
 
-            if (e.InitialPressMouseButton == MouseButton.Right)
+        private static void ControlContextRequested(object sender, ContextRequestedEventArgs e)
+        {
+            if (sender is Control control
+                && control.ContextMenu is ContextMenu contextMenu
+                && !e.Handled
+                && !contextMenu.CancelOpening())
             {
-                if (contextMenu.CancelOpening())
-                    return;
-
-                contextMenu.Open(control, e.Source as Control ?? control);
+                var requestedByPointer = e.TryGetPosition(null, out _);
+                contextMenu.Open(control, e.Source as Control ?? control, requestedByPointer);
                 e.Handled = true;
             }
         }
 
+        private static void ControlDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
+        {
+            if (sender is Control control
+                && control.ContextMenu is ContextMenu contextMenu)
+            {
+                contextMenu.Close();
+            }
+        }
+
         private bool CancelClosing()
         {
             var eventArgs = new CancelEventArgs();

+ 58 - 0
src/Avalonia.Controls/ContextRequestedEventArgs.cs

@@ -0,0 +1,58 @@
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides event data for the ContextRequested event.
+    /// </summary>
+    public class ContextRequestedEventArgs : RoutedEventArgs
+    {
+        private readonly PointerEventArgs? _pointerEventArgs;
+
+        /// <summary>
+        /// Initializes a new instance of the ContextRequestedEventArgs class.
+        /// </summary>
+        public ContextRequestedEventArgs()
+            : base(Control.ContextRequestedEvent)
+        {
+
+        }
+
+        /// <inheritdoc cref="ContextRequestedEventArgs()" />
+        public ContextRequestedEventArgs(PointerEventArgs pointerEventArgs)
+            : this()
+        {
+            _pointerEventArgs = pointerEventArgs;
+        }
+
+        /// <summary>
+        /// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied <see cref="Control"/>.
+        /// </summary>
+        /// <param name="relativeTo">
+        /// Any <see cref="Control"/>-derived object that is connected to the same object tree.
+        /// To specify the object relative to the overall coordinate system, use a relativeTo  value of null.
+        /// </param>
+        /// <param name="point">
+        /// A <see cref="Point"/> that represents the current x- and y-coordinates of the mouse pointer position.
+        /// If null was passed as relativeTo, this coordinate is for the overall window.
+        /// If a relativeTo value other than null was passed, this coordinate is relative to the object referenced by relativeTo.
+        /// </param>
+        /// <returns>
+        /// true if the context request was initiated by a pointer device; otherwise, false.
+        /// </returns>
+        public bool TryGetPosition(Control? relativeTo, out Point point)
+        {
+            if (_pointerEventArgs is null)
+            {
+                point = default;
+                return false;
+            }
+
+            point = _pointerEventArgs.GetPosition(relativeTo);
+            return true;
+        }
+    }
+}

+ 62 - 0
src/Avalonia.Controls/Control.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Rendering;
 using Avalonia.Styling;
@@ -19,6 +20,7 @@ namespace Avalonia.Controls
     /// The control class extends <see cref="InputElement"/> and adds the following features:
     ///
     /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
+    /// - <see cref="ContextRequestedEvent"/> and other context menu related members.
     /// </remarks>
     public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
     {
@@ -52,6 +54,13 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
             RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
 
+        /// <summary>
+        /// Provides event data for the <see cref="ContextRequested"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
+            RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested),
+                RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
         private DataTemplates? _dataTemplates;
         private IControl? _focusAdorner;
 
@@ -100,6 +109,15 @@ namespace Avalonia.Controls
             set => SetValue(TagProperty, value);
         }
 
+        /// <summary>
+        /// Occurs when the user has completed a context input gesture, such as a right-click.
+        /// </summary>
+        public event EventHandler<ContextRequestedEventArgs> ContextRequested
+        {
+            add => AddHandler(ContextRequestedEvent, value);
+            remove => RemoveHandler(ContextRequestedEvent, value);
+        }
+
         public new IControl? Parent => (IControl?)base.Parent;
 
         /// <inheritdoc/>
@@ -208,5 +226,49 @@ namespace Avalonia.Controls
                 _focusAdorner = null;
             }
         }
+
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+
+            if (e.Source == this
+                && !e.Handled
+                && e.InitialPressMouseButton == MouseButton.Right)
+            {
+                var args = new ContextRequestedEventArgs(e);
+                RaiseEvent(args);
+                e.Handled = args.Handled;
+            }
+        }
+
+        protected override void OnKeyUp(KeyEventArgs e)
+        {
+            base.OnKeyUp(e);
+
+            if (e.Source == this
+                && !e.Handled)
+            {
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>().OpenContextMenu;
+                var matches = false;
+
+                for (var index = 0; index < keymap.Count; index++)
+                {
+                    var key = keymap[index];
+                    matches |= key.Matches(e);
+
+                    if (matches)
+                    {
+                        break;
+                    }
+                }
+
+                if (matches)
+                {
+                    var args = new ContextRequestedEventArgs();
+                    RaiseEvent(args);
+                    e.Handled = args.Handled;
+                }
+            }
+        }
     }
 }

+ 5 - 5
src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs

@@ -26,13 +26,13 @@ namespace Avalonia.Controls.Converters
                     Right ? Indent * scalarDepth : 0,
                     Bottom ? Indent * scalarDepth : 0);
             }
-            else if (value is Thickness thinknessDepth)
+            else if (value is Thickness thicknessDepth)
             {
                 return new Thickness(
-                    Left ? Indent * thinknessDepth.Left : 0,
-                    Top ? Indent * thinknessDepth.Top : 0,
-                    Right ? Indent * thinknessDepth.Right : 0,
-                    Bottom ? Indent * thinknessDepth.Bottom : 0);
+                    Left ? Indent * thicknessDepth.Left : 0,
+                    Top ? Indent * thicknessDepth.Top : 0,
+                    Right ? Indent * thicknessDepth.Right : 0,
+                    Bottom ? Indent * thicknessDepth.Bottom : 0);
             }
             return new Thickness(0);
             

+ 2 - 2
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Controls.Converters
             if (parameter == null ||
                 values == null ||
                 values.Count != 4 ||
-                !(values[0] is ScrollBarVisibility visiblity) ||
+                !(values[0] is ScrollBarVisibility visibility) ||
                 !(values[1] is double offset) ||
                 !(values[2] is double extent) ||
                 !(values[3] is double viewport))
@@ -24,7 +24,7 @@ namespace Avalonia.Controls.Converters
                 return AvaloniaProperty.UnsetValue;
             }
 
-            if (visiblity == ScrollBarVisibility.Auto)
+            if (visibility == ScrollBarVisibility.Auto)
             {
                 if (extent == viewport)
                 {

+ 1 - 1
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -71,7 +71,7 @@ namespace Avalonia.Controls
                 x => x.MonthVisible, (x, v) => x.MonthVisible = v);
 
         /// <summary>
-        /// Defiens the <see cref="YearFormat"/> Property
+        /// Defines the <see cref="YearFormat"/> Property
         /// </summary>
         public static readonly DirectProperty<DatePicker, string> YearFormatProperty =
             AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(YearFormat), 

+ 6 - 6
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@@ -220,15 +220,15 @@ namespace Avalonia.Controls.Primitives
 
                 if (dy > 0) // Scroll Down
                 {
-                    int numContsToMove = 0;
+                    int numCountsToMove = 0;
                     for (int i = 0; i < children.Count; i++)
                     {
                         if (children[i].Bounds.Bottom - dy < 0)
-                            numContsToMove++;
+                            numCountsToMove++;
                         else
                             break;
                     }
-                    children.MoveRange(0, numContsToMove, children.Count);
+                    children.MoveRange(0, numCountsToMove, children.Count);
 
                     var scrollHeight = _extent.Height - Viewport.Height;
                     if (ShouldLoop && value.Y >= scrollHeight - _extentOne)
@@ -236,15 +236,15 @@ namespace Avalonia.Controls.Primitives
                 }
                 else if (dy < 0) // Scroll Up
                 {
-                    int numContsToMove = 0;
+                    int numCountsToMove = 0;
                     for (int i = children.Count - 1; i >= 0; i--)
                     {
                         if (children[i].Bounds.Top - dy > Bounds.Height)
-                            numContsToMove++;
+                            numCountsToMove++;
                         else
                             break;
                     }
-                    children.MoveRange(children.Count - numContsToMove, numContsToMove, 0);
+                    children.MoveRange(children.Count - numCountsToMove, numCountsToMove, 0);
                     if (ShouldLoop && value.Y < _extentOne)
                         _offset = new Vector(0, value.Y + (_extentOne * 50));
                 }

+ 4 - 4
src/Avalonia.Controls/DefinitionBase.cs

@@ -35,7 +35,7 @@ namespace Avalonia.Controls
             if (_sharedState == null)
             {
                 //  start with getting SharedSizeGroup value. 
-                //  this property is NOT inhereted which should result in better overall perf.
+                //  this property is NOT inherited which should result in better overall perf.
                 string sharedSizeGroupId = SharedSizeGroup;
                 if (sharedSizeGroupId != null)
                 {
@@ -52,7 +52,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Callback to notify about exitting model tree.
+        /// Callback to notify about exiting model tree.
         /// </summary>
         internal void OnExitParentTree()
         {
@@ -458,7 +458,7 @@ namespace Avalonia.Controls
         private Grid.LayoutTimeSizeType _sizeType;      //  layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content"
 
         private double _minSize;                        //  used during measure to accumulate size for "Auto" and "Star" DefinitionBase's
-        private double _measureSize;                    //  size, calculated to be the input contstraint size for Child.Measure
+        private double _measureSize;                    //  size, calculated to be the input constraint size for Child.Measure
         private double _sizeCache;                      //  cache used for various purposes (sorting, caching, etc) during calculations
         private double _offset;                         //  offset of the DefinitionBase from left / top corner (assuming LTR case)
 
@@ -556,7 +556,7 @@ namespace Avalonia.Controls
             }
 
             /// <summary>
-            /// Propogates invalidations for all registered definitions.
+            /// Propagates invalidations for all registered definitions.
             /// Resets its own state.
             /// </summary>
             internal void Invalidate()

+ 23 - 0
src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs

@@ -0,0 +1,23 @@
+using System;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Controls.Diagnostics
+{
+    /// <summary>
+    /// Diagnostics interface to retrieve an associated <see cref="IPopupHost"/>.
+    /// </summary>
+    public interface IPopupHostProvider
+    {
+        /// <summary>
+        /// The popup host.
+        /// </summary>
+        IPopupHost? PopupHost { get; }
+
+        /// <summary>
+        /// Raised when the popup host changes.
+        /// </summary>
+        event Action<IPopupHost?>? PopupHostChanged;
+    }
+}

+ 15 - 0
src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

@@ -0,0 +1,15 @@
+#nullable enable
+
+namespace Avalonia.Controls.Diagnostics
+{
+    /// <summary>
+    /// Helper class to provide diagnostics information for <see cref="ToolTip"/>.
+    /// </summary>
+    public static class ToolTipDiagnostics
+    {
+        /// <summary>
+        /// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools.
+        /// </summary>
+        public static AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty;
+    }
+}

+ 2 - 2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
             set
             {
                 _clientSize = value;
-                Resized?.Invoke(value);
+                Resized?.Invoke(value, PlatformResizeReason.Unspecified);
             }
         }
 
@@ -49,7 +49,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
         
         public Action<RawInputEventArgs> Input { get; set; }
         public Action<Rect> Paint { get; set; }
-        public Action<Size> Resized { get; set; }
+        public Action<Size, PlatformResizeReason> Resized { get; set; }
         public Action<double> ScalingChanged { get; set; }
 
         public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }

+ 132 - 53
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -1,16 +1,18 @@
 using System;
 using System.ComponentModel;
+using Avalonia.Controls.Diagnostics;
+using System.Linq;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Layout;
 using Avalonia.Logging;
-using Avalonia.Rendering;
 
 #nullable enable
 
 namespace Avalonia.Controls.Primitives
 {
-    public abstract class FlyoutBase : AvaloniaObject
+    public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
     {
         static FlyoutBase()
         {
@@ -49,14 +51,21 @@ namespace Avalonia.Controls.Primitives
         public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
             AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
 
+        private readonly Lazy<Popup> _popupLazy;
         private bool _isOpen;
         private Control? _target;
         private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
         private Rect? _enlargedPopupRect;
         private PixelRect? _enlargePopupRectScreenPixelRect;
         private IDisposable? _transientDisposable;
+        private Action<IPopupHost?>? _popupHostChangedHandler;
 
-        protected Popup? Popup { get; private set; }
+        public FlyoutBase()
+        {
+            _popupLazy = new Lazy<Popup>(() => CreatePopup());
+        }
+
+        protected Popup Popup => _popupLazy.Value;
 
         /// <summary>
         /// Gets whether this Flyout is currently Open
@@ -94,6 +103,14 @@ namespace Avalonia.Controls.Primitives
             private set => SetAndRaise(TargetProperty, ref _target, value);
         }
 
+        IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
+
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged 
+        { 
+            add => _popupHostChangedHandler += value; 
+            remove => _popupHostChangedHandler -= value;
+        }
+
         public event EventHandler? Closed;
         public event EventHandler<CancelEventArgs>? Closing;
         public event EventHandler? Opened;
@@ -142,22 +159,19 @@ namespace Avalonia.Controls.Primitives
             HideCore();
         }
 
-        protected virtual void HideCore(bool canCancel = true)
+        /// <returns>True, if action was handled</returns>
+        protected virtual bool HideCore(bool canCancel = true)
         {
             if (!IsOpen)
             {
-                return;
+                return false;
             }
 
             if (canCancel)
             {
-                bool cancel = false;
-
-                var closing = new CancelEventArgs();
-                Closing?.Invoke(this, closing);
-                if (cancel || closing.Cancel)
+                if (CancelClosing())
                 {
-                    return;
+                    return false;
                 }
             }
 
@@ -170,31 +184,42 @@ namespace Avalonia.Controls.Primitives
             _enlargedPopupRect = null;
             _enlargePopupRectScreenPixelRect = null;
 
+            if (Target != null)
+            {
+                Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
+                Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
+            }
+
             OnClosed();
+
+            return true;
         }
 
-        protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
+        /// <returns>True, if action was handled</returns>
+        protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
         {
             if (placementTarget == null)
-                throw new ArgumentNullException("placementTarget cannot be null");
-
-            if (Popup == null)
             {
-                InitPopup();
+                throw new ArgumentNullException(nameof(placementTarget));
             }
 
             if (IsOpen)
             {
                 if (placementTarget == Target)
                 {
-                    return;
+                    return false;
                 }
                 else // Close before opening a new one
                 {
-                    HideCore(false);
+                    _ = HideCore(false);
                 }
             }
 
+            if (CancelOpening())
+            {
+                return false;
+            }
+
             if (Popup.Parent != null && Popup.Parent != placementTarget)
             {
                 ((ISetLogicalParent)Popup).SetParent(null);
@@ -211,11 +236,13 @@ namespace Avalonia.Controls.Primitives
                 Popup.Child = CreatePresenter();
             }
 
-            OnOpening();
             PositionPopup(showAtPointer);
-            IsOpen = Popup.IsOpen = true;            
+            IsOpen = Popup.IsOpen = true;
             OnOpened();
-                        
+
+            placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
+            placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
+
             if (ShowMode == FlyoutShowMode.Standard)
             {
                 // Try and focus content inside Flyout
@@ -236,6 +263,13 @@ namespace Avalonia.Controls.Primitives
             {
                 _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
             }
+
+            return true;
+        }
+
+        private void PlacementTarget_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
+        {
+            _ = HideCore(false);
         }
 
         private void HandleTransientDismiss(RawInputEventArgs args)
@@ -254,7 +288,7 @@ namespace Avalonia.Controls.Primitives
                 {
                     // Only do this once when the Flyout opens & cache the result
                     if (Popup?.Host is PopupRoot root)
-                    { 
+                    {
                         // Get the popup root bounds and convert to screen coordinates
                         
                         var tmp = root.Bounds.Inflate(100);
@@ -294,9 +328,9 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        protected virtual void OnOpening()
+        protected virtual void OnOpening(CancelEventArgs args)
         {
-            Opening?.Invoke(this, null);
+            Opening?.Invoke(this, args);
         }
 
         protected virtual void OnOpened()
@@ -320,30 +354,63 @@ namespace Avalonia.Controls.Primitives
         /// <returns></returns>
         protected abstract Control CreatePresenter();
 
-        private void InitPopup()
+        private Popup CreatePopup()
         {
-            Popup = new Popup();
-            Popup.WindowManagerAddShadowHint = false;
-            Popup.IsLightDismissEnabled = true;
-
-            Popup.Opened += OnPopupOpened;
-            Popup.Closed += OnPopupClosed;
+            var popup = new Popup();
+            popup.WindowManagerAddShadowHint = false;
+            popup.IsLightDismissEnabled = true;
+            popup.OverlayDismissEventPassThrough = true;
+
+            popup.Opened += OnPopupOpened;
+            popup.Closed += OnPopupClosed;
+            popup.Closing += OnPopupClosing;
+            popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
+            return popup;
         }
 
         private void OnPopupOpened(object sender, EventArgs e)
         {
             IsOpen = true;
+
+            _popupHostChangedHandler?.Invoke(Popup!.Host);
+        }
+
+        private void OnPopupClosing(object sender, CancelEventArgs e)
+        {
+            if (IsOpen)
+            {
+                e.Cancel = CancelClosing();
+            }
         }
 
         private void OnPopupClosed(object sender, EventArgs e)
         {
-            HideCore();
+            HideCore(false);
+
+            _popupHostChangedHandler?.Invoke(null);
+        }
+
+        // This method is handling both popup logical tree and target logical tree.
+        private void OnPlacementTargetOrPopupKeyUp(object sender, KeyEventArgs e)
+        {
+            if (!e.Handled
+                && IsOpen
+                && Target?.ContextFlyout == this)
+            {
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
+
+                if (keymap.OpenContextMenu.Any(k => k.Matches(e)))
+                {
+                    e.Handled = HideCore();
+                }
+            }
         }
 
         private void PositionPopup(bool showAtPointer)
         {
             Size sz;
-            if(Popup.Child.DesiredSize == Size.Empty)
+            // Popup.Child can't be null here, it was set in ShowAtCore.
+            if (Popup.Child!.DesiredSize == Size.Empty)
             {
                 // Popup may not have been shown yet. Measure content
                 sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@@ -370,19 +437,19 @@ namespace Avalonia.Controls.Primitives
             switch (Placement)
             {
                 case FlyoutPlacementMode.Top: //Above & centered
-                    Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
+                    Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1);
                     Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
                     Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
                     break;
 
                 case FlyoutPlacementMode.TopEdgeAlignedLeft:
                     Popup.PlacementRect = new Rect(0, 0, 0, 0);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;                    
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
                     break;
 
                 case FlyoutPlacementMode.TopEdgeAlignedRight:
                     Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;                    
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
                     break;
 
                 case FlyoutPlacementMode.RightEdgeAlignedTop:
@@ -454,38 +521,50 @@ namespace Avalonia.Controls.Primitives
             {
                 if (args.OldValue is FlyoutBase)
                 {
-                    c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
+                    c.ContextRequested -= OnControlContextRequested;
                 }
                 if (args.NewValue is FlyoutBase)
                 {
-                    c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
+                    c.ContextRequested += OnControlContextRequested;
                 }
             }
         }
 
-        private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
+        private static void OnControlContextRequested(object sender, ContextRequestedEventArgs e)
         {
-            if (sender is Control c)
+            var control = (Control)sender;
+            if (!e.Handled
+                && control.ContextFlyout is FlyoutBase flyout)
             {
-                if (e.InitialPressMouseButton == MouseButton.Right &&
-                e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
+                if (control.ContextMenu != null)
                 {
-                    if (c.ContextFlyout != null)
-                    {
-                        if (c.ContextMenu != null)
-                        {
-                            Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
-                            return;
-                        }
-                        c.ContextFlyout.ShowAt(c, true);
-                    }
+                    Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
+                    return;
                 }
-            }            
+
+                // We do not support absolute popup positioning yet, so we ignore "point" at this moment.
+                var triggeredByPointerInput = e.TryGetPosition(null, out _);
+                e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput);
+            }
+        }
+
+        private bool CancelClosing()
+        {
+            var eventArgs = new CancelEventArgs();
+            OnClosing(eventArgs);
+            return eventArgs.Cancel;
+        }
+
+        private bool CancelOpening()
+        {
+            var eventArgs = new CancelEventArgs();
+            OnOpening(eventArgs);
+            return eventArgs.Cancel;
         }
 
         internal static void SetPresenterClasses(IControl presenter, Classes classes)
         {
-            //Remove any classes no longer in use, ignoring pseudoclasses
+            //Remove any classes no longer in use, ignoring pseudo classes
             for (int i = presenter.Classes.Count - 1; i >= 0; i--)
             {
                 if (!classes.Contains(presenter.Classes[i]) &&

+ 0 - 9
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@@ -6,15 +6,6 @@ namespace Avalonia.Controls
 {
     public class FlyoutPresenter : ContentControl
     {
-        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
-            Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
-
-        public CornerRadius CornerRadius
-        {
-            get => GetValue(CornerRadiusProperty);
-            set => SetValue(CornerRadiusProperty, value);
-        }
-
         protected override void OnKeyDown(KeyEventArgs e)
         {
             if (e.Key == Key.Escape)

+ 1 - 1
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@@ -12,7 +12,7 @@
         Standard,
 
         /// <summary>
-        /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
+        /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus.
         /// </summary>
         Transient,
 

+ 1 - 9
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@@ -29,16 +29,8 @@ namespace Avalonia.Controls
             var host = this.FindLogicalAncestorOfType<Popup>();
             if (host != null)
             {
-                for (int i = 0; i < LogicalChildren.Count; i++)
-                {
-                    if (LogicalChildren[i] is MenuItem item)
-                    {
-                        item.IsSubMenuOpen = false;
-                    }
-                }
-
                 SelectedIndex = -1;
-                host.IsOpen = false;                
+                host.IsOpen = false;
             }
         }
 

+ 9 - 9
src/Avalonia.Controls/Grid.cs

@@ -330,7 +330,7 @@ namespace Avalonia.Controls
                     //  value of Auto column), "cell 2 1" needs to be calculated first,
                     //  as it contributes to the Auto column's calculated value.
                     //  At the same time in order to accurately calculate constraint
-                    //  height for "cell 2 1", "cell 1 2" needs to be calcualted first,
+                    //  height for "cell 2 1", "cell 1 2" needs to be calculated first,
                     //  as it contributes to Auto row height, which is used in the
                     //  computation of Star row resolved height.
                     //
@@ -405,11 +405,11 @@ namespace Avalonia.Controls
                     //
                     //  where:
                     //  *   all [Measure GroupN] - regular children measure process -
-                    //      each cell is measured given contraint size as an input
+                    //      each cell is measured given constraint size as an input
                     //      and each cell's desired size is accumulated on the
                     //      corresponding column / row;
                     //  *   [Measure Group2'] - is when each cell is measured with
-                    //      infinit height as a constraint and a cell's desired
+                    //      infinite height as a constraint and a cell's desired
                     //      height is ignored;
                     //  *   [Measure Groups''] - is when each cell is measured (second
                     //      time during single Grid.MeasureOverride) regularly but its
@@ -780,7 +780,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection
+        /// Initializes DefinitionsU member either to user supplied ColumnDefinitions collection
         /// or to a default single element collection. DefinitionsU gets trimmed to size.
         /// </summary>
         /// <remarks>
@@ -821,7 +821,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection
+        /// Initializes DefinitionsV member either to user supplied RowDefinitions collection
         /// or to a default single element collection. DefinitionsV gets trimmed to size.
         /// </summary>
         /// <remarks>
@@ -2132,7 +2132,7 @@ namespace Avalonia.Controls
                 //
                 // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer)
                 // each being allocated a large number of pixels (~50 or greater), and
-                // people don't even notice the kind of 1-pixel anomolies that are
+                // people don't even notice the kind of 1-pixel anomalies that are
                 // theoretically inevitable, or don't care if they do.  At least they shouldn't
                 // care - no one should be using the results WPF's grid layout to make
                 // quantitative decisions; its job is to produce a reasonable display, not
@@ -2597,7 +2597,7 @@ namespace Avalonia.Controls
             if (scale < 0.0)
             {
                 // if one of the *-weights is Infinity, adjust the weights by mapping
-                // Infinty to 1.0 and everything else to 0.0:  the infinite items share the
+                // Infinity to 1.0 and everything else to 0.0:  the infinite items share the
                 // available space equally, everyone else gets nothing.
                 return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0;
             }
@@ -2655,7 +2655,7 @@ namespace Avalonia.Controls
         private enum Flags
         {
             //
-            //  the foolowing flags let grid tracking dirtiness in more granular manner:
+            //  the following flags let grid tracking dirtiness in more granular manner:
             //  * Valid???Structure flags indicate that elements were added or removed.
             //  * Valid???Layout flags indicate that layout time portion of the information
             //    stored on the objects should be updated.
@@ -2684,7 +2684,7 @@ namespace Avalonia.Controls
 
         /// <summary>
         /// ShowGridLines property. This property is used mostly
-        /// for simplification of visual debuggig. When it is set
+        /// for simplification of visual debugging. When it is set
         /// to <c>true</c> grid lines are drawn to visualize location
         /// of grid lines.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/ListBox.cs

@@ -89,7 +89,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <remarks>
         /// Note that the selection mode only applies to selections made via user interaction.
-        /// Multiple selections can be made programatically regardless of the value of this property.
+        /// Multiple selections can be made programmatically regardless of the value of this property.
         /// </remarks>
         public new SelectionMode SelectionMode
         {

+ 1 - 1
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@@ -3,7 +3,7 @@
 namespace Avalonia.Controls
 {
 
-    [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")]
+    [Obsolete("This class exists to maintain backwards compatibility with existing code. Use NativeMenuItemSeparator instead")]
     public class NativeMenuItemSeperator : NativeMenuItemSeparator 
     {
     }

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

@@ -25,7 +25,7 @@ namespace Avalonia.Platform
 
         /// <summary>
         /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used.
-        /// This is because Windows Chrome can not be shown ontop of user content.
+        /// This is because Windows Chrome can not be shown on top of user content.
         /// </summary>
         PreferSystemChrome = 0x02,
 

+ 17 - 0
src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs

@@ -0,0 +1,17 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Controls.ApplicationLifetimes;
+
+namespace Avalonia.Platform
+{
+    public interface IPlatformLifetimeEventsImpl
+    {
+        /// <summary>
+        /// Raised by the platform when a shutdown is requested.
+        /// </summary>
+        /// <remarks>
+        /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit.
+        /// </remarks>
+        event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
+    }
+}

+ 36 - 1
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -3,11 +3,46 @@ using System.Collections.Generic;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Layout;
 using Avalonia.Rendering;
 using JetBrains.Annotations;
 
 namespace Avalonia.Platform
 {
+    /// <summary>
+    /// Describes the reason for a <see cref="ITopLevelImpl.Resized"/> message.
+    /// </summary>
+    public enum PlatformResizeReason
+    {
+        /// <summary>
+        /// The resize reason is unknown or unspecified.
+        /// </summary>
+        Unspecified,
+
+        /// <summary>
+        /// The resize was due to the user resizing the window, for example by dragging the
+        /// window frame.
+        /// </summary>
+        User,
+
+        /// <summary>
+        /// The resize was initiated by the application, for example by setting one of the sizing-
+        /// related properties on <see cref="Window"/> such as <see cref="Layoutable.Width"/> or
+        /// <see cref="Layoutable.Height"/>.
+        /// </summary>
+        Application,
+
+        /// <summary>
+        /// The resize was initiated by the layout system.
+        /// </summary>
+        Layout,
+
+        /// <summary>
+        /// The resize was due to a change in DPI.
+        /// </summary>
+        DpiChange,
+    }
+
     /// <summary>
     /// Defines a platform-specific top-level window implementation.
     /// </summary>
@@ -57,7 +92,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// Gets or sets a method called when the toplevel is resized.
         /// </summary>
-        Action<Size> Resized { get; set; }
+        Action<Size, PlatformResizeReason> Resized { get; set; }
 
         /// <summary>
         /// Gets or sets a method called when the toplevel's scaling changes.

+ 3 - 1
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@@ -7,7 +7,9 @@ namespace Avalonia.Platform
         /// <summary>
         /// Shows the window.
         /// </summary>
-        void Show(bool activate);
+        /// <param name="activate">Whether to activate the shown window.</param>
+        /// <param name="isDialog">Whether the window is being shown as a dialog.</param>
+        void Show(bool activate, bool isDialog);
 
         /// <summary>
         /// Hides the window.

+ 3 - 1
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -110,7 +110,9 @@ namespace Avalonia.Platform
         /// <summary>
         /// Sets the client size of the top level.
         /// </summary>
-        void Resize(Size clientSize);
+        /// <param name="clientSize">The new client size.</param>
+        /// <param name="reason">The reason for the resize.</param>
+        void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application);
 
         /// <summary>
         /// Sets the client size of the top level.

+ 4 - 2
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@@ -21,10 +21,12 @@ namespace Avalonia.Controls.Platform
 
         public void RunLoop(CancellationToken cancellationToken)
         {
-            while (true)
+            var handles = new[] { _signaled, cancellationToken.WaitHandle };
+
+            while (!cancellationToken.IsCancellationRequested)
             {
                 Signaled?.Invoke(null);
-                _signaled.WaitOne();
+                WaitHandle.WaitAny(handles);
             }
         }
 

+ 48 - 19
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -1,19 +1,33 @@
 using System;
 using System.Collections.Specialized;
-using System.Linq;
-using System.Resources;
 using Avalonia.Media;
 using Avalonia.Rendering;
-using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Controls.Primitives
 {
-    // TODO: Need to track position of adorned elements and move the adorner if they move.
+    /// <summary>
+    /// Represents a surface for showing adorners.
+    /// Adorners are always on top of the adorned element and are positioned to stay relative to the adorned element.
+    /// </summary>
+    /// <remarks>
+    /// TODO: Need to track position of adorned elements and move the adorner if they move.
+    /// </remarks>
     public class AdornerLayer : Canvas, ICustomSimpleHitTest
     {
-        public static readonly AttachedProperty<Visual> AdornedElementProperty =
-            AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement");
+        /// <summary>
+        /// Allows for getting and setting of the adorned element.
+        /// </summary>
+        public static readonly AttachedProperty<Visual?> AdornedElementProperty =
+            AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual?>("AdornedElement");
+
+        /// <summary>
+        /// Allows for controlling clipping of the adorner.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsClipEnabledProperty =
+            AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, bool>("IsClipEnabled", true);
 
         private static readonly AttachedProperty<AdornedElementInfo> s_adornedElementInfoProperty =
             AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
@@ -28,7 +42,7 @@ namespace Avalonia.Controls.Primitives
             Children.CollectionChanged += ChildrenCollectionChanged;
         }
 
-        public static Visual GetAdornedElement(Visual adorner)
+        public static Visual? GetAdornedElement(Visual adorner)
         {
             return adorner.GetValue(AdornedElementProperty);
         }
@@ -38,12 +52,19 @@ namespace Avalonia.Controls.Primitives
             adorner.SetValue(AdornedElementProperty, adorned);
         }
 
-        public static AdornerLayer GetAdornerLayer(IVisual visual)
+        public static AdornerLayer? GetAdornerLayer(IVisual visual)
+        {
+            return visual.FindAncestorOfType<VisualLayerManager>()?.AdornerLayer;
+        }
+
+        public static bool GetIsClipEnabled(Visual adorner)
         {
-            return visual.GetVisualAncestors()
-                .OfType<VisualLayerManager>()
-                .FirstOrDefault()
-                ?.AdornerLayer;
+            return adorner.GetValue(IsClipEnabledProperty);
+        }
+
+        public static void SetIsClipEnabled(Visual adorner, bool isClipEnabled)
+        {
+            adorner.SetValue(IsClipEnabledProperty, isClipEnabled);
         }
 
         protected override Size MeasureOverride(Size availableSize)
@@ -70,12 +91,13 @@ namespace Avalonia.Controls.Primitives
             foreach (var child in Children)
             {
                 var info = child.GetValue(s_adornedElementInfoProperty);
+                var isClipEnabled = child.GetValue(IsClipEnabledProperty);
 
                 if (info != null && info.Bounds.HasValue)
                 {
                     child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
                     child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
-                    UpdateClip(child, info.Bounds.Value);
+                    UpdateClip(child, info.Bounds.Value, isClipEnabled);
                     child.Arrange(info.Bounds.Value.Bounds);
                 }
                 else
@@ -87,16 +109,23 @@ namespace Avalonia.Controls.Primitives
             return finalSize;
         }
 
-        private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e)
+        private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs<Visual?> e)
         {
             var adorner = (Visual)e.Sender;
-            var adorned = (Visual)e.NewValue;
+            var adorned = e.NewValue.GetValueOrDefault();
             var layer = adorner.GetVisualParent<AdornerLayer>();
             layer?.UpdateAdornedElement(adorner, adorned);
         }
 
-        private void UpdateClip(IControl control, TransformedBounds bounds)
+        private void UpdateClip(IControl control, TransformedBounds bounds, bool isEnabled)
         {
+            if (!isEnabled)
+            {
+                control.Clip = null;
+
+                return;
+            }
+
             if (!(control.Clip is RectangleGeometry clip))
             {
                 clip = new RectangleGeometry();
@@ -129,13 +158,13 @@ namespace Avalonia.Controls.Primitives
             InvalidateArrange();
         }
 
-        private void UpdateAdornedElement(Visual adorner, Visual adorned)
+        private void UpdateAdornedElement(Visual adorner, Visual? adorned)
         {
             var info = adorner.GetValue(s_adornedElementInfoProperty);
 
             if (info != null)
             {
-                info.Subscription.Dispose();
+                info.Subscription!.Dispose();
 
                 if (adorned == null)
                 {
@@ -163,7 +192,7 @@ namespace Avalonia.Controls.Primitives
 
         private class AdornedElementInfo
         {
-            public IDisposable Subscription { get; set; }
+            public IDisposable? Subscription { get; set; }
 
             public TransformedBounds? Bounds { get; set; }
         }

+ 0 - 5
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@@ -140,10 +140,5 @@ namespace Avalonia.Controls.Primitives
 
             return new OverlayPopupHost(overlayLayer);
         }
-
-        public override void Render(DrawingContext context)
-        {
-            context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
-        }
     }
 }

+ 25 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -1,6 +1,8 @@
 using System;
+using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Disposables;
+using Avalonia.Controls.Diagnostics;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
@@ -17,7 +19,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Displays a popup window.
     /// </summary>
-    public class Popup : Control, IVisualTreeHost
+    public class Popup : Control, IVisualTreeHost, IPopupHostProvider
     {
         public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
             AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true);
@@ -133,6 +135,7 @@ namespace Avalonia.Controls.Primitives
         private bool _ignoreIsOpenChanged;
         private PopupOpenState? _openState;
         private IInputElement _overlayInputPassThroughElement;
+        private Action<IPopupHost?>? _popupHostChangedHandler;
 
         /// <summary>
         /// Initializes static members of the <see cref="Popup"/> class.
@@ -154,6 +157,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public event EventHandler? Opened;
 
+        internal event EventHandler<CancelEventArgs>? Closing;
+
         public IPopupHost? Host => _openState?.PopupHost;
 
         public bool WindowManagerAddShadowHint
@@ -348,6 +353,14 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
 
+        IPopupHost? IPopupHostProvider.PopupHost => Host;
+
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged 
+        { 
+            add => _popupHostChangedHandler += value; 
+            remove => _popupHostChangedHandler -= value;
+        }
+
         /// <summary>
         /// Opens the popup.
         /// </summary>
@@ -479,6 +492,8 @@ namespace Avalonia.Controls.Primitives
             }
 
             Opened?.Invoke(this, EventArgs.Empty);
+
+            _popupHostChangedHandler?.Invoke(Host);
         }
 
         /// <summary>
@@ -567,6 +582,13 @@ namespace Avalonia.Controls.Primitives
 
         private void CloseCore()
         {
+            var closingArgs = new CancelEventArgs();
+            Closing?.Invoke(this, closingArgs);
+            if (closingArgs.Cancel)
+            {
+                return;
+            }
+
             _isOpenRequested = false;
             if (_openState is null)
             {
@@ -581,6 +603,8 @@ namespace Avalonia.Controls.Primitives
             _openState.Dispose();
             _openState = null;
 
+            _popupHostChangedHandler?.Invoke(null);
+
             using (BeginIgnoringIsOpen())
             {
                 IsOpen = false;

+ 1 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
     /// <summary>
     /// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be
-    /// aritrarily positioned.
+    /// arbitrarily positioned.
     /// </summary>
     public class ManagedPopupPositioner : IPopupPositioner
     {

+ 3 - 6
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -161,12 +161,9 @@ namespace Avalonia.Controls.Primitives
 
         protected override sealed Size ArrangeSetBounds(Size size)
         {
-            using (BeginAutoSizing())
-            {
-                _positionerParameters.Size = size;
-                UpdatePosition();
-                return ClientSize;
-            }
+            _positionerParameters.Size = size;
+            UpdatePosition();
+            return ClientSize;
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Primitives/RangeBase.cs

@@ -170,7 +170,7 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Checks if the double value is not inifinity nor NaN.
+        /// Checks if the double value is not infinity nor NaN.
         /// </summary>
         /// <param name="value">The value.</param>
         private static bool ValidateDouble(double value)

+ 32 - 2
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -57,6 +57,18 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<bool> AllowAutoHideProperty =
             AvaloniaProperty.Register<ScrollBar, bool>(nameof(AllowAutoHide), true);
 
+        /// <summary>
+        /// Defines the <see cref="HideDelay"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TimeSpan> HideDelayProperty =
+            AvaloniaProperty.Register<ScrollBar, TimeSpan>(nameof(HideDelay), TimeSpan.FromSeconds(2));
+
+        /// <summary>
+        /// Defines the <see cref="ShowDelay"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TimeSpan> ShowDelayProperty =
+            AvaloniaProperty.Register<ScrollBar, TimeSpan>(nameof(ShowDelay), TimeSpan.FromSeconds(0.5));
+
         private Button _lineUpButton;
         private Button _lineDownButton;
         private Button _pageUpButton;
@@ -126,6 +138,24 @@ namespace Avalonia.Controls.Primitives
             get => GetValue(AllowAutoHideProperty);
             set => SetValue(AllowAutoHideProperty, value);
         }
+        
+        /// <summary>
+        /// Gets a value that determines how long will be the hide delay after user stops interacting with the scrollbar.
+        /// </summary>
+        public TimeSpan HideDelay
+        {
+            get => GetValue(HideDelayProperty);
+            set => SetValue(HideDelayProperty, value);
+        }
+        
+        /// <summary>
+        /// Gets a value that determines how long will be the show delay when user starts interacting with the scrollbar.
+        /// </summary>
+        public TimeSpan ShowDelay
+        {
+            get => GetValue(ShowDelayProperty);
+            set => SetValue(ShowDelayProperty, value);
+        }
 
         public event EventHandler<ScrollEventArgs> Scroll;
 
@@ -296,12 +326,12 @@ namespace Avalonia.Controls.Primitives
 
         private void CollapseAfterDelay()
         {
-            InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2));
+            InvokeAfterDelay(Collapse, HideDelay);
         }
 
         private void ExpandAfterDelay()
         {
-            InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400));
+            InvokeAfterDelay(Expand, ShowDelay);
         }
 
         private void Collapse()

+ 2 - 2
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -96,7 +96,7 @@ namespace Avalonia.Controls.Primitives
         /// Defines the <see cref="IsTextSearchEnabled"/> property.
         /// </summary>
         public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
-            AvaloniaProperty.Register<ItemsControl, bool>(nameof(IsTextSearchEnabled), true);
+            AvaloniaProperty.Register<ItemsControl, bool>(nameof(IsTextSearchEnabled), false);
 
         /// <summary>
         /// Event that should be raised by items that implement <see cref="ISelectable"/> to
@@ -328,7 +328,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <remarks>
         /// Note that the selection mode only applies to selections made via user interaction.
-        /// Multiple selections can be made programatically regardless of the value of this property.
+        /// Multiple selections can be made programmatically regardless of the value of this property.
         /// </remarks>
         protected SelectionMode SelectionMode
         {

+ 29 - 11
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -5,7 +5,8 @@ using Avalonia.Logging;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Styling;
-using Avalonia.VisualTree;
+
+#nullable enable
 
 namespace Avalonia.Controls.Primitives
 {
@@ -17,13 +18,13 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Background"/> property.
         /// </summary>
-        public static readonly StyledProperty<IBrush> BackgroundProperty =
+        public static readonly StyledProperty<IBrush?> BackgroundProperty =
             Border.BackgroundProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
         /// Defines the <see cref="BorderBrush"/> property.
         /// </summary>
-        public static readonly StyledProperty<IBrush> BorderBrushProperty =
+        public static readonly StyledProperty<IBrush?> BorderBrushProperty =
             Border.BorderBrushProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
@@ -32,6 +33,12 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
 
+        /// <summary>
+        /// Defines the <see cref="CornerRadius"/> property.
+        /// </summary>
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            Border.CornerRadiusProperty.AddOwner<TemplatedControl>();
+
         /// <summary>
         /// Defines the <see cref="FontFamily"/> property.
         /// </summary>
@@ -59,7 +66,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Foreground"/> property.
         /// </summary>
-        public static readonly StyledProperty<IBrush> ForegroundProperty =
+        public static readonly StyledProperty<IBrush?> ForegroundProperty =
             TextBlock.ForegroundProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
@@ -71,8 +78,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Template"/> property.
         /// </summary>
-        public static readonly StyledProperty<IControlTemplate> TemplateProperty =
-            AvaloniaProperty.Register<TemplatedControl, IControlTemplate>(nameof(Template));
+        public static readonly StyledProperty<IControlTemplate?> TemplateProperty =
+            AvaloniaProperty.Register<TemplatedControl, IControlTemplate?>(nameof(Template));
 
         /// <summary>
         /// Defines the IsTemplateFocusTarget attached property.
@@ -88,7 +95,7 @@ namespace Avalonia.Controls.Primitives
                 "TemplateApplied", 
                 RoutingStrategies.Direct);
 
-        private IControlTemplate _appliedTemplate;
+        private IControlTemplate? _appliedTemplate;
 
         /// <summary>
         /// Initializes static members of the <see cref="TemplatedControl"/> class.
@@ -111,7 +118,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the brush used to draw the control's background.
         /// </summary>
-        public IBrush Background
+        public IBrush? Background
         {
             get { return GetValue(BackgroundProperty); }
             set { SetValue(BackgroundProperty, value); }
@@ -120,7 +127,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the brush used to draw the control's border.
         /// </summary>
-        public IBrush BorderBrush
+        public IBrush? BorderBrush
         {
             get { return GetValue(BorderBrushProperty); }
             set { SetValue(BorderBrushProperty, value); }
@@ -135,6 +142,15 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(BorderThicknessProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the radius of the border rounded corners.
+        /// </summary>
+        public CornerRadius CornerRadius
+        {
+            get { return GetValue(CornerRadiusProperty); }
+            set { SetValue(CornerRadiusProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the font family used to draw the control's text.
         /// </summary>
@@ -174,7 +190,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the brush used to draw the control's text and other foreground elements.
         /// </summary>
-        public IBrush Foreground
+        public IBrush? Foreground
         {
             get { return GetValue(ForegroundProperty); }
             set { SetValue(ForegroundProperty, value); }
@@ -192,7 +208,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the template that defines the control's appearance.
         /// </summary>
-        public IControlTemplate Template
+        public IControlTemplate? Template
         {
             get { return GetValue(TemplateProperty); }
             set { SetValue(TemplateProperty, value); }
@@ -265,7 +281,9 @@ namespace Avalonia.Controls.Primitives
 
                     var e = new TemplateAppliedEventArgs(nameScope);
                     OnApplyTemplate(e);
+#pragma warning disable CS0618 // Type or member is obsolete
                     OnTemplateApplied(e);
+#pragma warning restore CS0618 // Type or member is obsolete
                     RaiseEvent(e);
                 }
 

+ 20 - 0
src/Avalonia.Controls/Primitives/Thumb.cs

@@ -56,6 +56,26 @@ namespace Avalonia.Controls.Primitives
         {
         }
 
+        protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+        {
+            if (_lastPoint.HasValue)
+            {
+                var ev = new VectorEventArgs
+                {
+                    RoutedEvent = DragCompletedEvent,
+                    Vector = _lastPoint.Value,
+                };
+
+                _lastPoint = null;
+
+                RaiseEvent(ev);
+            }
+
+            PseudoClasses.Remove(":pressed");
+
+            base.OnPointerCaptureLost(e);
+        }
+
         protected override void OnPointerMoved(PointerEventArgs e)
         {
             if (_lastPoint.HasValue)

+ 1 - 1
src/Avalonia.Controls/Repeater/IElementFactory.cs

@@ -46,7 +46,7 @@ namespace Avalonia.Controls
     }
 
     /// <summary>
-    /// A data template that supports creating and recyling elements for an <see cref="ItemsRepeater"/>.
+    /// A data template that supports creating and recycling elements for an <see cref="ItemsRepeater"/>.
     /// </summary>
     public interface IElementFactory : IDataTemplate
     {

+ 2 - 2
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -441,9 +441,9 @@ namespace Avalonia.Controls
             base.OnPropertyChanged(change);
         }
 
-        internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
+        internal IControl GetElementImpl(int index, bool forceCreate, bool suppressAutoRecycle)
         {
-            var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
+            var element = _viewManager.GetElement(index, forceCreate, suppressAutoRecycle);
             return element;
         }
 

+ 1 - 1
src/Avalonia.Controls/Repeater/ViewManager.cs

@@ -174,7 +174,7 @@ namespace Avalonia.Controls
             }
             else
             {
-                // We could not find a candiate.
+                // We could not find a candidate.
                 _lastFocusedElement = null;
             }
         }

+ 1 - 1
src/Avalonia.Controls/Repeater/ViewportManager.cs

@@ -186,7 +186,7 @@ namespace Avalonia.Controls
                 _expectedViewportShift.X + _layoutExtent.X - extent.X,
                 _expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
 
-            // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
+            // We tolerate viewport imprecisions up to 1 pixel to avoid invalidating layout too much.
             if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
             {
                 Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})",

+ 1 - 1
src/Avalonia.Controls/ScrollViewer.cs

@@ -653,7 +653,7 @@ namespace Avalonia.Controls
         private void CalculatedPropertiesChanged()
         {
             // Pass old values of 0 here because we don't have the old values at this point,
-            // and it shouldn't matter as only the template uses these properies.
+            // and it shouldn't matter as only the template uses these properties.
             RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum);
             RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue);
             RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize);

+ 1 - 1
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -345,7 +345,7 @@ namespace Avalonia.Controls.Selection
         private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
         {
             // Note: We're *not* putting this in a using scope. A collection update is still in progress
-            // so the operation won't get commited by normal means: we have to commit it manually.
+            // so the operation won't get committed by normal means: we have to commit it manually.
             var update = BatchUpdate();
 
             update.Operation.DeselectedItems = deselectedItems;

+ 61 - 25
src/Avalonia.Controls/TextBox.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Controls
 
         public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current
             .GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault();
-        
+
         public static readonly StyledProperty<bool> AcceptsReturnProperty =
             AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn));
 
@@ -117,7 +117,7 @@ namespace Avalonia.Controls
 
         public static readonly StyledProperty<bool> RevealPasswordProperty =
             AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
-        
+
         public static readonly DirectProperty<TextBox, bool> CanCutProperty =
             AvaloniaProperty.RegisterDirect<TextBox, bool>(
                 nameof(CanCut),
@@ -135,7 +135,7 @@ namespace Avalonia.Controls
 
         public static readonly StyledProperty<bool> IsUndoEnabledProperty =
             AvaloniaProperty.Register<TextBox, bool>(
-                nameof(IsUndoEnabled), 
+                nameof(IsUndoEnabled),
                 defaultValue: true);
 
         public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
@@ -157,6 +157,10 @@ namespace Avalonia.Controls
             }
 
             public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text);
+
+            public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other);
+
+            public override int GetHashCode() => Text.GetHashCode();
         }
 
         private string _text;
@@ -174,6 +178,10 @@ namespace Avalonia.Controls
         private string _newLine = Environment.NewLine;
         private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
 
+        private int _selectedTextChangesMadeSinceLastUndoSnapshot;
+        private bool _hasDoneSnapshotOnce;
+        private const int _maxCharsBeforeUndoSnapshot = 7;
+
         static TextBox()
         {
             FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
@@ -202,7 +210,8 @@ namespace Avalonia.Controls
                 horizontalScrollBarVisibility,
                 BindingPriority.Style);
             _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
-
+            _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+            _hasDoneSnapshotOnce = false;
             UpdatePseudoclasses();
         }
 
@@ -331,6 +340,7 @@ namespace Avalonia.Controls
                     if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
                     {
                         _undoRedoHelper.Clear();
+                        SnapshotUndoRedo(); // so we always have an initial state
                     }
                 }
             }
@@ -341,16 +351,16 @@ namespace Avalonia.Controls
             get { return GetSelection(); }
             set
             {
-                SnapshotUndoRedo();
                 if (string.IsNullOrEmpty(value))
                 {
+                    _selectedTextChangesMadeSinceLastUndoSnapshot++;
+                    SnapshotUndoRedo(ignoreChangeCount: false);
                     DeleteSelection();
                 }
                 else
                 {
                     HandleTextInput(value);
                 }
-                SnapshotUndoRedo();
             }
         }
 
@@ -422,7 +432,7 @@ namespace Avalonia.Controls
             get { return _newLine; }
             set { SetAndRaise(NewLineProperty, ref _newLine, value); }
         }
-        
+
         /// <summary>
         /// Clears the current selection, maintaining the <see cref="CaretIndex"/>
         /// </summary>
@@ -480,11 +490,13 @@ namespace Avalonia.Controls
                     var oldValue = _undoRedoHelper.Limit;
                     _undoRedoHelper.Limit = value;
                     RaisePropertyChanged(UndoLimitProperty, oldValue, value);
-                } 
+                }
                 // from docs at
                 // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
                 // "Setting UndoLimit clears the undo queue."
                 _undoRedoHelper.Clear();
+                _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+                _hasDoneSnapshotOnce = false;
             }
         }
 
@@ -515,6 +527,8 @@ namespace Avalonia.Controls
                 // Therefore, if you disable undo and then re-enable it, undo commands still do not work
                 // because the undo stack was emptied when you disabled undo."
                 _undoRedoHelper.Clear();
+                _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+                _hasDoneSnapshotOnce = false;
             }
         }
 
@@ -577,23 +591,25 @@ namespace Avalonia.Controls
             {
                 return;
             }
-            
+
             input = RemoveInvalidCharacters(input);
-            
+
             if (string.IsNullOrEmpty(input))
             {
                 return;
             }
-            
+            _selectedTextChangesMadeSinceLastUndoSnapshot++;
+            SnapshotUndoRedo(ignoreChangeCount: false);
+
             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();
@@ -627,7 +643,6 @@ namespace Avalonia.Controls
             SnapshotUndoRedo();
             Copy();
             DeleteSelection();
-            SnapshotUndoRedo();
         }
 
         public async void Copy()
@@ -647,7 +662,6 @@ namespace Avalonia.Controls
 
             SnapshotUndoRedo();
             HandleTextInput(text);
-            SnapshotUndoRedo();
         }
 
         protected override void OnKeyDown(KeyEventArgs e)
@@ -696,6 +710,7 @@ namespace Avalonia.Controls
             {
                 try
                 {
+                    SnapshotUndoRedo();
                     _isUndoingRedoing = true;
                     _undoRedoHelper.Undo();
                 }
@@ -830,7 +845,6 @@ namespace Avalonia.Controls
                             CaretIndex -= removedCharacters;
                             ClearSelection();
                         }
-                        SnapshotUndoRedo();
 
                         handled = true;
                         break;
@@ -858,7 +872,6 @@ namespace Avalonia.Controls
                             SetTextInternal(text.Substring(0, caretIndex) +
                                             text.Substring(caretIndex + removedCharacters));
                         }
-                        SnapshotUndoRedo();
 
                         handled = true;
                         break;
@@ -868,7 +881,6 @@ namespace Avalonia.Controls
                         {
                             SnapshotUndoRedo();
                             HandleTextInput(NewLine);
-                            SnapshotUndoRedo();
                             handled = true;
                         }
 
@@ -879,7 +891,6 @@ namespace Avalonia.Controls
                         {
                             SnapshotUndoRedo();
                             HandleTextInput("\t");
-                            SnapshotUndoRedo();
                             handled = true;
                         }
                         else
@@ -889,6 +900,10 @@ namespace Avalonia.Controls
 
                         break;
 
+                    case Key.Space:
+                        SnapshotUndoRedo(); // always snapshot in between words
+                        break;
+
                     default:
                         handled = false;
                         break;
@@ -1098,6 +1113,11 @@ namespace Avalonia.Controls
 
         private bool MoveVertical(int count)
         {
+            if (_presenter is null)
+            {
+                return false;
+            }
+
             var formattedText = _presenter.FormattedText;
             var lines = formattedText.GetLines().ToList();
             var caretIndex = CaretIndex;
@@ -1113,14 +1133,17 @@ namespace Avalonia.Controls
                 CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
                 return true;
             }
-            else
-            {
-                return false;
-            }
+
+            return false;
         }
 
         private void MoveHome(bool document)
         {
+            if (_presenter is null)
+            {
+                return;
+            }
+            
             var text = Text ?? string.Empty;
             var caretIndex = CaretIndex;
 
@@ -1151,6 +1174,11 @@ namespace Avalonia.Controls
 
         private void MoveEnd(bool document)
         {
+            if (_presenter is null)
+            {
+                return;
+            }
+            
             var text = Text ?? string.Empty;
             var caretIndex = CaretIndex;
 
@@ -1306,11 +1334,19 @@ namespace Avalonia.Controls
             }
         }
 
-        private void SnapshotUndoRedo()
+        private void SnapshotUndoRedo(bool ignoreChangeCount = true)
         {
             if (IsUndoEnabled)
             {
-                _undoRedoHelper.Snapshot();
+                if (ignoreChangeCount ||
+                    !_hasDoneSnapshotOnce ||
+                    (!ignoreChangeCount &&
+                        _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot))
+                {
+                    _undoRedoHelper.Snapshot();
+                    _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+                    _hasDoneSnapshotOnce = true;
+                }
             }
         }
     }

+ 4 - 4
src/Avalonia.Controls/TickBar.cs

@@ -193,7 +193,7 @@ namespace Avalonia.Controls
 
         /// <summary>
         /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or
-        /// top and bottom spacing (for vertical orienation).
+        /// top and bottom spacing (for vertical orientation).
         /// The space on both sides of TickBar is half of specified ReservedSpace.
         /// This property has type of <see cref="Rect" />.
         /// </summary>
@@ -210,7 +210,7 @@ namespace Avalonia.Controls
         /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and
         /// SelectionStart and SelectionEnd are valid.
         ///
-        /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height
+        /// The primary ticks (for Minimum and Maximum value) height will be 100% of TickBar's render size (use Width or Height
         /// depends on Placement property).
         ///
         /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size.
@@ -221,7 +221,7 @@ namespace Avalonia.Controls
         {
             var size = new Size(Bounds.Width, Bounds.Height);
             var range = Maximum - Minimum;
-            var tickLen = 0.0d;   // Height for Primary Tick (for Mininum and Maximum value)
+            var tickLen = 0.0d;   // Height for Primary Tick (for Minimum and Maximum value)
             var tickLen2 = 0.0d;  // Height for Secondary Tick
             var logicalToPhysical = 1.0;
             var startPoint = new Point();
@@ -285,7 +285,7 @@ namespace Avalonia.Controls
 
             tickLen2 = tickLen * 0.75;
 
-            // Invert direciton of the ticks
+            // Invert direction of the ticks
             if (IsDirectionReversed)
             {
                 logicalToPhysical *= -1;

+ 25 - 15
src/Avalonia.Controls/ToolTip.cs

@@ -1,9 +1,8 @@
 #nullable enable
 using System;
-using System.Reactive.Linq;
+using Avalonia.Controls.Diagnostics;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -17,7 +16,7 @@ namespace Avalonia.Controls
     /// assigning the content that you want displayed.
     /// </remarks>
     [PseudoClasses(":open")]
-    public class ToolTip : ContentControl
+    public class ToolTip : ContentControl, IPopupHostProvider
     {
         /// <summary>
         /// Defines the ToolTip.Tip attached property.
@@ -61,7 +60,8 @@ namespace Avalonia.Controls
         internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
             AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
 
-        private IPopupHost? _popup;
+        private IPopupHost? _popupHost;
+        private Action<IPopupHost?>? _popupHostChangedHandler;
 
         /// <summary>
         /// Initializes static members of the <see cref="ToolTip"/> class.
@@ -251,35 +251,45 @@ namespace Avalonia.Controls
 
             tooltip.RecalculatePosition(control);
         }
+        
+        IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
+
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged 
+        { 
+            add => _popupHostChangedHandler += value; 
+            remove => _popupHostChangedHandler -= value;
+        }
 
         internal void RecalculatePosition(Control control)
         {
-            _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
+            _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
         }
 
         private void Open(Control control)
         {
             Close();
 
-            _popup = OverlayPopupHost.CreatePopupHost(control, null);
-            _popup.SetChild(this);
-            ((ISetLogicalParent)_popup).SetParent(control);
+            _popupHost = OverlayPopupHost.CreatePopupHost(control, null);
+            _popupHost.SetChild(this);
+            ((ISetLogicalParent)_popupHost).SetParent(control);
             
-            _popup.ConfigurePosition(control, GetPlacement(control), 
+            _popupHost.ConfigurePosition(control, GetPlacement(control), 
                 new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
 
-            WindowManagerAddShadowHintChanged(_popup, false);
+            WindowManagerAddShadowHintChanged(_popupHost, false);
 
-            _popup.Show();
+            _popupHost.Show();
+            _popupHostChangedHandler?.Invoke(_popupHost);
         }
 
         private void Close()
         {
-            if (_popup != null)
+            if (_popupHost != null)
             {
-                _popup.SetChild(null);
-                _popup.Dispose();
-                _popup = null;
+                _popupHost.SetChild(null);
+                _popupHost.Dispose();
+                _popupHost = null;
+                _popupHostChangedHandler?.Invoke(null);
             }
         }
 

+ 7 - 2
src/Avalonia.Controls/TopLevel.cs

@@ -90,6 +90,7 @@ namespace Avalonia.Controls
         /// </summary>
         static TopLevel()
         {
+            KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevel>(KeyboardNavigationMode.Cycle);
             AffectsMeasure<TopLevel>(ClientSizeProperty);
 
             TransparencyLevelHintProperty.Changed.AddClassHandler<TopLevel>(
@@ -224,7 +225,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets the acheived <see cref="WindowTransparencyLevel"/> that the platform was able to provide.
+        /// Gets the achieved <see cref="WindowTransparencyLevel"/> that the platform was able to provide.
         /// </summary>
         public WindowTransparencyLevel ActualTransparencyLevel
         {
@@ -376,11 +377,15 @@ namespace Avalonia.Controls
             LayoutManager?.Dispose();
         }
 
+        [Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
+        protected virtual void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
+
         /// <summary>
         /// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
         /// </summary>
         /// <param name="clientSize">The new client size.</param>
-        protected virtual void HandleResized(Size clientSize)
+        /// <param name="reason">The reason for the resize.</param>
+        protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason)
         {
             ClientSize = clientSize;
             FrameSize = PlatformImpl.FrameSize;

+ 2 - 9
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@@ -7,7 +7,7 @@ using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Utils
 {
-    class UndoRedoHelper<TState> : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable<TState>
+    class UndoRedoHelper<TState>
     {
         private readonly IUndoRedoHost _host;
 
@@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils
         public UndoRedoHelper(IUndoRedoHost host)
         {
             _host = host;
-            WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1));
         }
 
         public void Undo()
@@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils
             if (_states.Last != null)
             {
                 _states.Last.Value = state;
-            } 
+            }
         }
 
         public void UpdateLastState()
@@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils
             _states.Clear();
             _currentNode = null;
         }
-
-        bool WeakTimer.IWeakTimerSubscriber.Tick()
-        {
-            Snapshot();
-            return true;
-        }
     }
 }

+ 77 - 68
src/Avalonia.Controls/Window.cs

@@ -244,7 +244,7 @@ namespace Avalonia.Controls
             impl.WindowStateChanged = HandleWindowStateChanged;
             _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
             impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;            
-            this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
+            this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
 
             PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
         }
@@ -258,6 +258,18 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets a value indicating how the window will size itself to fit its content.
         /// </summary>
+        /// <remarks>
+        /// If <see cref="SizeToContent"/> has a value other than <see cref="SizeToContent.Manual"/>,
+        /// <see cref="SizeToContent"/> is automatically set to <see cref="SizeToContent.Manual"/>
+        /// if a user resizes the window by using the resize grip or dragging the border.
+        /// 
+        /// NOTE: Because of a limitation of X11, <see cref="SizeToContent"/> will be reset on X11 to
+        /// <see cref="SizeToContent.Manual"/> on any resize - including the resize that happens when
+        /// the window is first shown. This is because X11 resize notifications are asynchronous and
+        /// there is no way to know whether a resize came from the user or the layout system. To avoid
+        /// this, consider setting <see cref="CanResize"/> to false, which will disable user resizing
+        /// of the window.
+        /// </remarks>
         public SizeToContent SizeToContent
         {
             get { return GetValue(SizeToContentProperty); }
@@ -583,28 +595,23 @@ namespace Avalonia.Controls
                 return;
             }
 
-            using (BeginAutoSizing())
-            {
-                Renderer?.Stop();
+            Renderer?.Stop();
 
-                if (Owner is Window owner)
-                {
-                    owner.RemoveChild(this);
-                }
+            if (Owner is Window owner)
+            {
+                owner.RemoveChild(this);
+            }
 
-                if (_children.Count > 0)
+            if (_children.Count > 0)
+            {
+                foreach (var child in _children.ToArray())
                 {
-                    foreach (var child in _children.ToArray())
-                    {
-                        child.child.Hide();
-                    }
+                    child.child.Hide();
                 }
-
-                Owner = null;
-
-                PlatformImpl?.Hide();
             }
 
+            Owner = null;
+            PlatformImpl?.Hide();
             IsVisible = false;
         }
 
@@ -675,29 +682,23 @@ namespace Avalonia.Controls
 
             if (initialSize != ClientSize)
             {
-                using (BeginAutoSizing())
-                {
-                    PlatformImpl?.Resize(initialSize);
-                }
+                PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
             }
 
             LayoutManager.ExecuteInitialLayoutPass();
 
-            using (BeginAutoSizing())
+            if (parent != null)
             {
-                if (parent != null)
-                {
-                    PlatformImpl?.SetParent(parent.PlatformImpl);
-                }
-                
-                Owner = parent;
-                parent?.AddChild(this, false);
-                
-                SetWindowStartupLocation(Owner?.PlatformImpl);
-                
-                PlatformImpl?.Show(ShowActivated);
-                Renderer?.Start();                
+                PlatformImpl?.SetParent(parent.PlatformImpl);
             }
+
+            Owner = parent;
+            parent?.AddChild(this, false);
+
+            SetWindowStartupLocation(Owner?.PlatformImpl);
+
+            PlatformImpl?.Show(ShowActivated, false);
+            Renderer?.Start();
             OnOpened(EventArgs.Empty);
         }
 
@@ -760,41 +761,34 @@ namespace Avalonia.Controls
 
             if (initialSize != ClientSize)
             {
-                using (BeginAutoSizing())
-                {
-                    PlatformImpl?.Resize(initialSize);
-                }
+                PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
             }
 
             LayoutManager.ExecuteInitialLayoutPass();
 
             var result = new TaskCompletionSource<TResult>();
 
-            using (BeginAutoSizing())
-            {
-                PlatformImpl?.SetParent(owner.PlatformImpl);
-                Owner = owner;
-                owner.AddChild(this, true);
-                
-                SetWindowStartupLocation(owner.PlatformImpl);
-                
-                PlatformImpl?.Show(ShowActivated);
+            PlatformImpl?.SetParent(owner.PlatformImpl);
+            Owner = owner;
+            owner.AddChild(this, true);
 
-                Renderer?.Start();
+            SetWindowStartupLocation(owner.PlatformImpl);
 
-                Observable.FromEventPattern<EventHandler, EventArgs>(
-                        x => Closed += x,
-                        x => Closed -= x)
-                    .Take(1)
-                    .Subscribe(_ =>
-                    {
-                        owner.Activate();
-                        result.SetResult((TResult)(_dialogResult ?? default(TResult)));
-                    });
+            PlatformImpl?.Show(ShowActivated, true);
 
-                OnOpened(EventArgs.Empty);
-            }
+            Renderer?.Start();
+
+            Observable.FromEventPattern<EventHandler, EventArgs>(
+                    x => Closed += x,
+                    x => Closed -= x)
+                .Take(1)
+                .Subscribe(_ =>
+                {
+                    owner.Activate();
+                    result.SetResult((TResult)(_dialogResult ?? default(TResult)));
+                });
 
+            OnOpened(EventArgs.Empty);
             return result.Task;
         }
 
@@ -937,11 +931,8 @@ namespace Avalonia.Controls
 
         protected sealed override Size ArrangeSetBounds(Size size)
         {
-            using (BeginAutoSizing())
-            {
-                PlatformImpl?.Resize(size);
-                return ClientSize;
-            }
+            PlatformImpl?.Resize(size, PlatformResizeReason.Layout);
+            return ClientSize;
         }
 
         protected sealed override void HandleClosed()
@@ -958,18 +949,36 @@ namespace Avalonia.Controls
             Owner = null;
         }
 
+        [Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
+        protected sealed override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
+
         /// <inheritdoc/>
-        protected sealed override void HandleResized(Size clientSize)
+        protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason)
         {
-            if (!AutoSizing)
+            if (ClientSize == clientSize)
+                return;
+
+            var sizeToContent = SizeToContent;
+
+            // If auto-sizing is enabled, and the resize came from a user resize (or the reason was
+            // unspecified) then turn off auto-resizing for any window dimension that is not equal
+            // to the requested size.
+            if (sizeToContent != SizeToContent.Manual &&
+                CanResize &&
+                reason == PlatformResizeReason.Unspecified ||  
+                reason == PlatformResizeReason.User)
             {
-                SizeToContent = SizeToContent.Manual;
+                if (clientSize.Width != ClientSize.Width)
+                    sizeToContent &= ~SizeToContent.Width;
+                if (clientSize.Height != ClientSize.Height)
+                    sizeToContent &= ~SizeToContent.Height;
+                SizeToContent = sizeToContent;
             }
 
             Width = clientSize.Width;
             Height = clientSize.Height;
 
-            base.HandleResized(clientSize);
+            base.HandleResized(clientSize, reason);
         }
 
         /// <summary>

+ 12 - 22
src/Avalonia.Controls/WindowBase.cs

@@ -39,7 +39,6 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> TopmostProperty =
             AvaloniaProperty.Register<WindowBase, bool>(nameof(Topmost));
 
-        private int _autoSizing;
         private bool _hasExecutedInitialLayoutPass;
         private bool _isActive;
         private bool _ignoreVisibilityChange;
@@ -95,10 +94,8 @@ namespace Avalonia.Controls
         
         public Screens Screens { get; private set; }
 
-        /// <summary>
-        /// Whether an auto-size operation is in progress.
-        /// </summary>
-        protected bool AutoSizing => _autoSizing > 0;
+        [Obsolete("No longer used. Always returns false.")]
+        protected bool AutoSizing => false;
 
         /// <summary>
         /// Gets or sets the owner of the window.
@@ -162,7 +159,7 @@ namespace Avalonia.Controls
                     LayoutManager.ExecuteInitialLayoutPass();
                     _hasExecutedInitialLayoutPass = true;
                 }
-                PlatformImpl?.Show(true);
+                PlatformImpl?.Show(true, false);
                 Renderer?.Start();
                 OnOpened(EventArgs.Empty);
             }
@@ -172,20 +169,9 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <summary>
-        /// Begins an auto-resize operation.
-        /// </summary>
-        /// <returns>A disposable used to finish the operation.</returns>
-        /// <remarks>
-        /// When an auto-resize operation is in progress any resize events received will not be
-        /// cause the new size to be written to the <see cref="Layoutable.Width"/> and
-        /// <see cref="Layoutable.Height"/> properties.
-        /// </remarks>
-        protected IDisposable BeginAutoSizing()
-        {
-            ++_autoSizing;
-            return Disposable.Create(() => --_autoSizing);
-        }
+
+        [Obsolete("No longer used. Has no effect.")]
+        protected IDisposable BeginAutoSizing() => Disposable.Empty;
 
         /// <summary>
         /// Ensures that the window is initialized.
@@ -215,11 +201,15 @@ namespace Avalonia.Controls
             }
         }
 
+        [Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
+        protected override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
+
         /// <summary>
         /// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
         /// </summary>
         /// <param name="clientSize">The new client size.</param>
-        protected override void HandleResized(Size clientSize)
+        /// <param name="reason">The reason for the resize.</param>
+        protected override void HandleResized(Size clientSize, PlatformResizeReason reason)
         {
             ClientSize = clientSize;
             FrameSize = PlatformImpl.FrameSize;
@@ -264,7 +254,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Called durung the arrange pass to set the size of the window.
+        /// Called during the arrange pass to set the size of the window.
         /// </summary>
         /// <param name="size">The requested size of the window.</param>
         /// <returns>The actual size of the window.</returns>

+ 2 - 6
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.DesignerSupport.Remote
             ClientSize = new Size(1, 1);
         }
 
-        public void Show(bool activate)
+        public void Show(bool activate, bool isDialog)
         {
         }
 
@@ -58,7 +58,7 @@ namespace Avalonia.DesignerSupport.Remote
             base.OnMessage(transport, obj);
         }
         
-        public void Resize(Size clientSize)
+        public void Resize(Size clientSize, PlatformResizeReason reason)
         {
             _transport.Send(new RequestViewportResizeMessage
             {
@@ -99,10 +99,6 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void ShowDialog(IWindowImpl parent)
-        {
-        }
-
         public void SetSystemDecorations(SystemDecorations enabled)
         {
         }

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

@@ -27,7 +27,7 @@ namespace Avalonia.DesignerSupport.Remote
         public IEnumerable<object> Surfaces { get; }
         public Action<RawInputEventArgs> Input { get; set; }
         public Action<Rect> Paint { get; set; }
-        public Action<Size> Resized { get; set; }
+        public Action<Size, PlatformResizeReason> Resized { get; set; }
         public Action<double> ScalingChanged { get; set; }
         public Func<bool> Closing { get; set; }
         public Action Closed { get; set; }
@@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote
                 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent,
                     (_, size, __) =>
                     {
-                        Resize(size);
+                        Resize(size, PlatformResizeReason.Unspecified);
                     }));
         }
 
@@ -78,7 +78,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void Show(bool activate)
+        public void Show(bool activate, bool isDialog)
         {
         }
 
@@ -98,7 +98,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void Resize(Size clientSize)
+        public void Resize(Size clientSize, PlatformResizeReason reason)
         {
         }
 

+ 7 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -1,6 +1,5 @@
 using System;
 using System.ComponentModel;
-
 using Avalonia.Controls;
 using Avalonia.Diagnostics.Models;
 using Avalonia.Input;
@@ -22,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
         private bool _shouldVisualizeMarginPadding = true;
         private bool _shouldVisualizeDirtyRects;
         private bool _showFpsOverlay;
+        private bool _freezePopups;
 
 #nullable disable
         // Remove "nullable disable" after MemberNotNull will work on our CI.
@@ -41,6 +41,12 @@ namespace Avalonia.Diagnostics.ViewModels
             Console = new ConsoleViewModel(UpdateConsoleContext);
         }
 
+        public bool FreezePopups
+        {
+            get => _freezePopups;
+            set => RaiseAndSetIfChanged(ref _freezePopups, value);
+        }
+
         public bool ShouldVisualizeMarginPadding
         {
             get => _shouldVisualizeMarginPadding;

+ 13 - 20
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@@ -1,26 +1,28 @@
 using System;
-using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Reactive;
 using System.Reactive.Linq;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.LogicalTree;
+using Avalonia.Media;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal abstract class TreeNode : ViewModelBase, IDisposable
     {
-        private IDisposable? _classesSubscription;
+        private readonly IDisposable? _classesSubscription;
         private string _classes;
         private bool _isExpanded;
 
-        public TreeNode(IVisual visual, TreeNode? parent)
+        protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
         {
+            _classes = string.Empty;
             Parent = parent;
-            Type = visual.GetType().Name;
+            Type = customName ?? visual.GetType().Name;
             Visual = visual;
-            _classes = string.Empty;
+            FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
 
             if (visual is IControl control)
             {
@@ -52,6 +54,12 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
+        private bool IsRoot => Visual is TopLevel ||
+                               Visual is ContextMenu ||
+                               Visual is IPopupHost;
+
+        public FontWeight FontWeight { get; }
+
         public abstract TreeNodeCollection Children
         {
             get;
@@ -95,20 +103,5 @@ namespace Avalonia.Diagnostics.ViewModels
             _classesSubscription?.Dispose();
             Children.Dispose();
         }
-
-        private static int IndexOf(IReadOnlyList<TreeNode> collection, TreeNode item)
-        {
-            var count = collection.Count;
-
-            for (var i = 0; i < count; ++i)
-            {
-                if (collection[i] == item)
-                {
-                    return i;
-                }
-            }
-
-            throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
-        }
     }
 }

+ 109 - 14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@@ -1,5 +1,10 @@
 using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
 using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Diagnostics;
+using Avalonia.Controls.Primitives;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
 
@@ -7,31 +12,30 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class VisualTreeNode : TreeNode
     {
-        public VisualTreeNode(IVisual visual, TreeNode? parent)
-            : base(visual, parent)
+        public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
+            : base(visual, parent, customName)
         {
             Children = new VisualTreeNodeCollection(this, visual);
 
-            if ((Visual is IStyleable styleable))
-            {
+            if (Visual is IStyleable styleable)
                 IsInTemplate = styleable.TemplatedParent != null;
-            }
         }
 
-        public bool IsInTemplate { get; private set; }
+        public bool IsInTemplate { get; }
 
         public override TreeNodeCollection Children { get; }
 
         public static VisualTreeNode[] Create(object control)
         {
-            var visual = control as IVisual;
-            return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty<VisualTreeNode>();
+            return control is IVisual visual ?
+                new[] { new VisualTreeNode(visual, null) } :
+                Array.Empty<VisualTreeNode>();
         }
 
         internal class VisualTreeNodeCollection : TreeNodeCollection
         {
             private readonly IVisual _control;
-            private IDisposable? _subscription;
+            private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
 
             public VisualTreeNodeCollection(TreeNode owner, IVisual control)
                 : base(owner)
@@ -41,15 +45,106 @@ namespace Avalonia.Diagnostics.ViewModels
 
             public override void Dispose()
             {
-                _subscription?.Dispose();
+                _subscriptions.Dispose();
+            }
+
+            private static IObservable<PopupRoot?>? GetHostedPopupRootObservable(IVisual visual)
+            {
+                static IObservable<PopupRoot?> GetPopupHostObservable(
+                    IPopupHostProvider popupHostProvider,
+                    string? providerName = null)
+                {
+                    return Observable.FromEvent<IPopupHost?>(
+                            x => popupHostProvider.PopupHostChanged += x,
+                            x => popupHostProvider.PopupHostChanged -= x)
+                        .StartWith(popupHostProvider.PopupHost)
+                        .Select(popupHost =>
+                        {
+                            if (popupHost is IControl control)
+                                return new PopupRoot(
+                                    control,
+                                    providerName != null ? $"{providerName} ({control.GetType().Name})" : null);
+
+                            return (PopupRoot?)null;
+                        });
+                }
+
+                return visual switch
+                {
+                    Popup p => GetPopupHostObservable(p),
+                    Control c => Observable.CombineLatest(
+                            c.GetObservable(Control.ContextFlyoutProperty),
+                            c.GetObservable(Control.ContextMenuProperty),
+                            c.GetObservable(FlyoutBase.AttachedFlyoutProperty),
+                            c.GetObservable(ToolTipDiagnostics.ToolTipProperty),
+                            (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) =>
+                            {
+                                if (ContextMenu != null)
+                                    //Note: ContextMenus are special since all the items are added as visual children.
+                                    //So we don't need to go via Popup
+                                    return Observable.Return<PopupRoot?>(new PopupRoot(ContextMenu));
+
+                                if (ContextFlyout != null)
+                                    return GetPopupHostObservable(ContextFlyout, "ContextFlyout");
+
+                                if (AttachedFlyout != null)
+                                    return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout");
+
+                                if (ToolTip != null)
+                                    return GetPopupHostObservable(ToolTip, "ToolTip");
+
+                                return Observable.Return<PopupRoot?>(null);
+                            })
+                        .Switch(),
+                    _ => null
+                };
             }
 
             protected override void Initialize(AvaloniaList<TreeNode> nodes)
             {
-                _subscription = _control.VisualChildren.ForEachItem(
-                    (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
-                    (i, item) => nodes.RemoveAt(i),
-                    () => nodes.Clear());
+                _subscriptions.Clear();
+
+                if (GetHostedPopupRootObservable(_control) is { } popupRootObservable)
+                {
+                    VisualTreeNode? childNode = null;
+
+                    _subscriptions.Add(
+                        popupRootObservable
+                            .Subscribe(popupRoot =>
+                            {
+                                if (popupRoot != null)
+                                {
+                                    childNode = new VisualTreeNode(
+                                        popupRoot.Value.Root,
+                                        Owner,
+                                        popupRoot.Value.CustomName);
+
+                                    nodes.Add(childNode);
+                                }
+                                else if (childNode != null)
+                                {
+                                    nodes.Remove(childNode);
+                                }
+                            }));
+                }
+
+                _subscriptions.Add(
+                    _control.VisualChildren.ForEachItem(
+                        (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
+                        (i, item) => nodes.RemoveAt(i),
+                        () => nodes.Clear()));
+            }
+
+            private struct PopupRoot
+            {
+                public PopupRoot(IControl root, string? customName = null)
+                {
+                    Root = root;
+                    CustomName = customName;
+                }
+
+                public IControl Root { get; }
+                public string? CustomName { get; }
             }
         }
     }

Некоторые файлы не были показаны из-за большого количества измененных файлов