Browse Source

Merge branch 'master' into fixes/11076-layout-invalidation

Steven Kirk 2 years ago
parent
commit
3db7059d98
29 changed files with 507 additions and 40 deletions
  1. 17 0
      packages/Avalonia/Avalonia.csproj
  2. 2 0
      packages/Avalonia/Avalonia.props
  3. 2 8
      samples/Sandbox/MainWindow.axaml.cs
  4. 2 0
      samples/Sandbox/Sandbox.csproj
  5. 10 3
      src/Avalonia.Base/Input/Cursor.cs
  6. 23 0
      src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs
  7. 2 0
      src/Avalonia.Controls/DefinitionList.cs
  8. 24 4
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  9. 4 1
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  10. 7 1
      src/Avalonia.Controls/TopLevel.cs
  11. 4 2
      src/Avalonia.Controls/TreeViewItem.cs
  12. 7 2
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  13. 5 0
      src/Avalonia.Controls/VirtualizingPanel.cs
  14. 11 3
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  15. 24 4
      src/Avalonia.Controls/WindowBase.cs
  16. 107 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  17. 14 0
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  18. 19 0
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  19. 1 1
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  20. 1 1
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  21. 2 1
      src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml
  22. 14 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  23. 2 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  24. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  25. 65 0
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  26. 13 0
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  27. 42 7
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  28. 51 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  29. 31 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

+ 17 - 0
packages/Avalonia/Avalonia.csproj

@@ -5,6 +5,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+      <PackageReference Include="Avalonia.BuildServices" Version="0.0.12" />
       <ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
       <ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
         <PrivateAssets>all</PrivateAssets>
@@ -60,4 +61,20 @@
   <Import Project="..\..\build\NetFX.props" />
   <Import Project="..\..\build\CoreLibraries.props" />
   <Import Project="..\..\build\SourceLink.props" Condition="'$(DisableSourceLink)' == ''" />
+
+  <Target Name="WriteCurrentPackageVersionProps" BeforeTargets="_GetPackageFiles">
+    <PropertyGroup>
+      <PackageVersionPropsPath>$(IntermediateOutputPath)/AvaloniaVersion.props</PackageVersionPropsPath>
+    </PropertyGroup>
+    <WriteLinesToFile
+      File="$(PackageVersionPropsPath)"
+      Overwrite="true"
+      Lines="&lt;Project&gt;&lt;PropertyGroup&gt;&lt;AvaloniaMainPackageVersion&gt;$(PackageVersion)&lt;/AvaloniaMainPackageVersion&gt;&lt;/PropertyGroup&gt;&lt;/Project&gt;" />
+    <ItemGroup>
+      <Content Include="$(PackageVersionPropsPath)">
+        <Pack>true</Pack>
+        <PackagePath>build</PackagePath>
+      </Content>
+    </ItemGroup>
+  </Target>
 </Project>

+ 2 - 0
packages/Avalonia/Avalonia.props

@@ -4,9 +4,11 @@
     <AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
     <AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
     <AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>
+    <UsedAvaloniaProducts>$(UsedAvaloniaProducts);AvaloniaUI</UsedAvaloniaProducts>
   </PropertyGroup>
   <Import Project="$(MSBuildThisFileDirectory)\AvaloniaBuildTasks.props"/>
   <Import Project="$(MSBuildThisFileDirectory)\Avalonia.Generators.props"/>
+  <Import Project="$(MSBuildThisFileDirectory)..\build\AvaloniaVersion.props" />
 
   <!-- Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package -->
   <ItemGroup>

+ 2 - 8
samples/Sandbox/MainWindow.axaml.cs

@@ -6,17 +6,11 @@ using Avalonia.Win32.WinRT.Composition;
 
 namespace Sandbox
 {
-    public class MainWindow : Window
+    public partial class MainWindow : Window
     {
         public MainWindow()
         {
-            this.InitializeComponent();
-            this.AttachDevTools();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
+            InitializeComponent();
         }
     }
 }

+ 2 - 0
samples/Sandbox/Sandbox.csproj

@@ -4,6 +4,7 @@
     <OutputType>WinExe</OutputType>
     <TargetFramework>net6.0</TargetFramework>
     <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
   </PropertyGroup>
 
   <ItemGroup>
@@ -17,4 +18,5 @@
   <Import Project="..\..\build\SampleApp.props" />
   <Import Project="..\..\build\ReferenceCoreLibraries.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
+  <Import Project="..\..\build\SourceGenerators.props" />
 </Project>

+ 10 - 3
src/Avalonia.Base/Input/Cursor.cs

@@ -42,19 +42,21 @@ namespace Avalonia.Input
     public class Cursor : IDisposable
     {
         public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow);
+        private string _name;
 
-        internal Cursor(ICursorImpl platformImpl)
+        private Cursor(ICursorImpl platformImpl, string name)
         {
             PlatformImpl = platformImpl;
+            _name = name;
         }
 
         public Cursor(StandardCursorType cursorType)
-            : this(GetCursorFactory().GetCursor(cursorType))
+            : this(GetCursorFactory().GetCursor(cursorType), cursorType.ToString())
         {
         }
 
         public Cursor(IBitmap cursor, PixelPoint hotSpot)
-            : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot))
+            : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor")
         {
         }
 
@@ -73,5 +75,10 @@ namespace Avalonia.Input
         {
             return AvaloniaLocator.Current.GetRequiredService<ICursorFactory>();
         }
+
+        public override string ToString()
+        {
+            return _name;
+        }
     }
 }

+ 23 - 0
src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs

@@ -0,0 +1,23 @@
+using System;
+
+namespace Avalonia.Metadata;
+
+/// <summary>
+/// Defines how compiler should split avalonia list string value before parsing individual items.
+/// </summary>
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+public sealed class AvaloniaListAttribute : Attribute
+{
+    /// <summary>
+    /// Separator used to split input string.
+    /// Default value is ','.
+    /// </summary>
+    public string[]? Separators { get; init; }
+
+    /// <summary>
+    /// Split options used to split input string.
+    /// Default value is RemoveEmptyEntries with TrimEntries.
+    /// </summary>
+    // StringSplitOptions.TrimEntries = 2, but only on net6 target.
+    public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2;
+}

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

@@ -1,9 +1,11 @@
 using System.Collections;
 using System.Collections.Specialized;
 using Avalonia.Collections;
+using Avalonia.Metadata;
 
 namespace Avalonia.Controls
 {
+    [AvaloniaList(Separators = new [] { ",", " " })]
     public abstract class DefinitionList<T> : AvaloniaList<T> where T : DefinitionBase
     {
         public DefinitionList()

+ 24 - 4
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -7,8 +7,8 @@ namespace Avalonia.Controls.Generators
     /// </summary>
     /// <remarks>
     /// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
-    /// method order should be followed:
-    /// 
+    /// process should be followed:
+    ///
     /// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
     ///   derived from the <see cref="Control"/> class. If this method returns true then the
     ///   item itself should be used as the container.
@@ -19,9 +19,29 @@ namespace Avalonia.Controls.Generators
     /// - The container should then be added to the panel using 
     ///   <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
     /// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
-    /// - When the item is ready to be recycled, <see cref="ClearItemContainer(Control)"/> should
-    ///   be called if <see cref="IsItemItsOwnContainer(Control)"/> returned false.
     /// 
+    /// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> in the first step above returns true
+    /// then the above steps should be carried out a single time; the first time the item is 
+    /// displayed. Otherwise the steps should be carried out each time a new container is realized
+    /// for an item.
+    ///
+    /// When unrealizing a container, the following process should be followed:
+    /// 
+    /// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item
+    ///   cannot be unrealized or recycled.
+    /// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container
+    /// - If recycling is supported then the container should be added to a recycle pool.
+    /// - It is assumed that recyclable containers will not be removed from the panel but instead
+    ///   hidden from view using e.g. `container.IsVisible = false`.
+    ///
+    /// When recycling an unrealized container, the following process should be followed:
+    /// 
+    /// - An element should be taken from the recycle pool.
+    /// - The container should be made visible.
+    /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
+    ///   container.
+    /// - <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
+    ///
     /// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only
     /// concerns itself with generating and clearing item containers; it does not maintain a
     /// record of the currently realized containers, that responsibility is delegated to the

+ 4 - 1
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@@ -67,9 +67,12 @@ namespace Avalonia.Controls.Presenters
                 for (var i = 0; i < count; ++i)
                 {
                     var c = children[index + i];
+
                     if (!c.IsSet(ItemIsOwnContainerProperty))
+                    {
                         itemsControl.RemoveLogicalChild(children[i + index]);
-                    generator.ClearItemContainer(c);
+                        generator.ClearItemContainer(c);
+                    }
                 }
 
                 children.RemoveRange(index, count);

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

@@ -285,6 +285,11 @@ namespace Avalonia.Controls
         /// </summary>
         public event EventHandler? Closed;
 
+        /// <summary>
+        /// Gets or sets a method called when the TopLevel's scaling changes.
+        /// </summary>
+        public event EventHandler? ScalingChanged;
+        
         /// <summary>
         /// Gets or sets the client size of the window.
         /// </summary>
@@ -428,7 +433,7 @@ namespace Avalonia.Controls
         double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
 
         /// <inheritdoc/>
-        double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
+        public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
 
         IStyleHost IStyleHost.StylingParent => _globalStyles!;
         
@@ -590,6 +595,7 @@ namespace Avalonia.Controls
         protected virtual void HandleScalingChanged(double scaling)
         {
             LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
+            ScalingChanged?.Invoke(this, EventArgs.Empty);
         }
 
         private static bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received)

+ 4 - 2
src/Avalonia.Controls/TreeViewItem.cs

@@ -45,6 +45,7 @@ namespace Avalonia.Controls
 
         private TreeView? _treeView;
         private Control? _header;
+        private Control? _headerPresenter;
         private int _level;
         private bool _templateApplied;
         private bool _deferredBringIntoViewFlag;
@@ -255,15 +256,16 @@ namespace Avalonia.Controls
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
-            if (_header is InputElement previousInputMethod)
+            if (_headerPresenter is InputElement previousInputMethod)
             {
                 previousInputMethod.DoubleTapped -= HeaderDoubleTapped;
             }
 
             _header = e.NameScope.Find<Control>("PART_Header");
+            _headerPresenter = e.NameScope.Find<Control>("PART_HeaderPresenter");
             _templateApplied = true;
 
-            if (_header is InputElement im)
+            if (_headerPresenter is InputElement im)
             {
                 im.DoubleTapped += HeaderDoubleTapped;
             }

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

@@ -168,7 +168,13 @@ namespace Avalonia.Controls
 
         protected internal override Control? ContainerFromIndex(int index)
         {
-            return index == _realizedIndex ? _realized : null;
+            if (index < 0 || index >= Items.Count)
+                return null;
+            if (index == _realizedIndex)
+                return _realized;
+            if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
+                return c;
+            return null;
         }
 
         protected internal override IEnumerable<Control>? GetRealizedContainers()
@@ -264,7 +270,6 @@ namespace Avalonia.Controls
                 if (controlItem.IsSet(ItemIsOwnContainerProperty))
                 {
                     controlItem.IsVisible = true;
-                    generator.ItemContainerPrepared(controlItem, item, index);
                     return controlItem;
                 }
                 else if (generator.IsItemItsOwnContainer(controlItem))

+ 5 - 0
src/Avalonia.Controls/VirtualizingPanel.cs

@@ -76,6 +76,11 @@ namespace Avalonia.Controls
         /// The container for the item at the specified index within the item collection, if the
         /// item is realized; otherwise, null.
         /// </returns>
+        /// <remarks>
+        /// Note for implementors: if the item at the the specified index is an ItemIsOwnContainer
+        /// item that has previously been realized, then the item should be returned even if it
+        /// currently falls outside the realized viewport.
+        /// </remarks>
         protected internal abstract Control? ContainerFromIndex(int index);
 
         /// <summary>

+ 11 - 3
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Diagnostics;
 using System.Linq;
-using System.Reflection;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
 using Avalonia.Input;
@@ -326,7 +325,17 @@ namespace Avalonia.Controls
             return _realizedElements?.Elements.Where(x => x is not null)!;
         }
 
-        protected internal override Control? ContainerFromIndex(int index) => _realizedElements?.GetElement(index);
+        protected internal override Control? ContainerFromIndex(int index)
+        {
+            if (index < 0 || index >= Items.Count)
+                return null;
+            if (_realizedElements?.GetElement(index) is { } realized)
+                return realized;
+            if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
+                return c;
+            return null;
+        }
+
         protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
 
         protected internal override Control? ScrollIntoView(int index)
@@ -577,7 +586,6 @@ namespace Avalonia.Controls
                 if (controlItem.IsSet(ItemIsOwnContainerProperty))
                 {
                     controlItem.IsVisible = true;
-                    generator.ItemContainerPrepared(controlItem, item, index);
                     return controlItem;
                 }
                 else if (generator.IsItemItsOwnContainer(controlItem))

+ 24 - 4
src/Avalonia.Controls/WindowBase.cs

@@ -83,6 +83,19 @@ namespace Avalonia.Controls
         /// <summary>
         /// Occurs when the window is resized.
         /// </summary>
+        /// <remarks>
+        /// Although this event is similar to the <see cref="Control.SizeChanged"/> event, they are
+        /// conceptually different:
+        /// 
+        /// - <see cref="Resized"/> is a window-level event, fired when a resize notification arrives
+        ///   from the platform windowing subsystem. The event args contain details of the source of
+        ///   the resize event in the <see cref="WindowResizedEventArgs.Reason"/> property. This
+        ///   event is raised before layout has been run on the window's content.
+        /// - <see cref="Control.SizeChanged"/> is a layout-level event, fired when a layout pass
+        ///   completes on a control. <see cref="Control.SizeChanged"/> is present on all controls
+        ///   and is fired when the control's size changes for any reason, including a
+        ///   <see cref="Resized"/> event in the case of a Window.
+        /// </remarks>
         public event EventHandler<WindowResizedEventArgs>? Resized;
 
         public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl;
@@ -116,6 +129,11 @@ namespace Avalonia.Controls
             set { SetValue(TopmostProperty, value); }
         }
 
+        /// <summary>
+        /// Gets the scaling factor for Window positioning and sizing.
+        /// </summary>
+        public double DesktopScaling => PlatformImpl?.DesktopScaling ?? 1;
+        
         /// <summary>
         /// Activates the window.
         /// </summary>
@@ -232,14 +250,16 @@ namespace Avalonia.Controls
         {
             FrameSize = PlatformImpl?.FrameSize;
 
-            if (ClientSize != clientSize)
+            var clientSizeChanged = ClientSize != clientSize;
+
+            ClientSize = clientSize;
+            OnResized(new WindowResizedEventArgs(clientSize, reason));
+
+            if (clientSizeChanged)
             {
-                ClientSize = clientSize;
                 LayoutManager.ExecuteLayoutPass();
                 Renderer.Resized(clientSize);
             }
-
-            OnResized(new WindowResizedEventArgs(clientSize, reason));
         }
 
         /// <summary>

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

@@ -1,5 +1,11 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.LogicalTree;
+using Avalonia.Metadata;
+using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
@@ -21,6 +27,8 @@ namespace Avalonia.Diagnostics.ViewModels
             SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters();
         }
 
+        public event EventHandler<string>? ClipboardCopyRequested;
+        
         public MainViewModel MainView { get; }
 
         public FilterViewModel PropertiesFilter { get; }
@@ -106,6 +114,105 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
+        public void CopySelector()
+        {
+            var currentVisual = SelectedNode?.Visual as Visual;
+            if (currentVisual is not null)
+            {
+                var selector = GetVisualSelector(currentVisual);
+                
+                ClipboardCopyRequested?.Invoke(this, selector);
+            }
+        }
+        
+        public void CopySelectorFromTemplateParent()
+        {
+            var parts = new List<string>();
+
+            var currentVisual = SelectedNode?.Visual as Visual;
+            while (currentVisual is not null)
+            {
+                parts.Add(GetVisualSelector(currentVisual));
+                
+                currentVisual = currentVisual.TemplatedParent as Visual;
+            }
+
+            if (parts.Any())
+            {
+                parts.Reverse();
+                var selector = string.Join(" /template/ ", parts);
+
+                ClipboardCopyRequested?.Invoke(this, selector);
+            }
+        }
+
+        public void ExpandRecursively()
+        {
+            if (SelectedNode is { } selectedNode)
+            {
+                ExpandNode(selectedNode);
+                
+                var stack = new Stack<TreeNode>();
+                stack.Push(selectedNode);
+
+                while (stack.Count > 0)
+                {
+                    var item = stack.Pop();
+                    item.IsExpanded = true;
+                    foreach (var child in item.Children)
+                    {
+                        stack.Push(child);
+                    }
+                }
+            }
+        }
+
+        public void CollapseChildren()
+        {
+            if (SelectedNode is { } selectedNode)
+            {
+                var stack = new Stack<TreeNode>();
+                stack.Push(selectedNode);
+
+                while (stack.Count > 0)
+                {
+                    var item = stack.Pop();
+                    item.IsExpanded = false;
+                    foreach (var child in item.Children)
+                    {
+                        stack.Push(child);
+                    }
+                }
+            }
+        }
+
+        public void CaptureNodeScreenshot()
+        {
+            MainView.Shot(null);
+        }
+
+        public void BringIntoView()
+        {
+            (SelectedNode?.Visual as Control)?.BringIntoView();
+        }
+        
+        
+        public void Focus()
+        {
+            (SelectedNode?.Visual as Control)?.Focus();
+        }
+
+        private static string GetVisualSelector(Visual visual)
+        {
+            var name = string.IsNullOrEmpty(visual.Name) ? "" : $"#{visual.Name}";
+            var classes = string.Concat(visual.Classes
+                .Where(c => !c.StartsWith(":"))
+                .Select(c => '.' + c));
+            var typeName = ((IStyleable)visual).StyleKey.Name;
+
+            return $"{typeName}{name}{classes}";
+        } 
+
         private void ExpandNode(TreeNode? node)
         {
             if (node != null)

+ 14 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@@ -26,6 +26,20 @@
           <Setter Property="Background" Value="Transparent" />
         </Style>
       </TreeView.Styles>
+      <TreeView.ContextFlyout>
+        <MenuFlyout>
+          <MenuItem Header="Copy">
+            <MenuItem Header="Copy individual node selector" Command="{Binding CopySelector}" />
+            <MenuItem Header="Copy selector from template parent" Command="{Binding CopySelectorFromTemplateParent}" />
+          </MenuItem>
+          <MenuItem Header="-" />
+          <MenuItem Header="Expand recursively" Command="{Binding ExpandRecursively}" />
+          <MenuItem Header="Collapse children" Command="{Binding CollapseChildren}" />
+          <MenuItem Header="Capture node screenshot" Command="{Binding CaptureNodeScreenshot}" />
+          <MenuItem Header="Bring into view" Command="{Binding BringIntoView}" />
+          <MenuItem Header="Focus" Command="{Binding Focus}" />
+        </MenuFlyout>
+      </TreeView.ContextFlyout>
     </TreeView>
 
     <GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1"/>

+ 19 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Diagnostics;
 using System.Linq;
 using Avalonia.Controls;
@@ -97,9 +98,27 @@ namespace Avalonia.Diagnostics.Views
             _currentLayer = null;
         }
 
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == DataContextProperty)
+            {
+                if (change.GetOldValue<object?>() is TreePageViewModel oldViewModel)
+                    oldViewModel.ClipboardCopyRequested -= OnClipboardCopyRequested;
+                if (change.GetNewValue<object?>() is TreePageViewModel newViewModel)
+                    newViewModel.ClipboardCopyRequested += OnClipboardCopyRequested;
+            }
+        }
+
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
         }
+
+        private void OnClipboardCopyRequested(object? sender, string e)
+        {
+            TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(e);
+        }
     }
 }

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

@@ -12,7 +12,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Tmds.DBus.Protocol" Version="0.14.0" />
+    <PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
     <PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.5" PrivateAssets="All" />
   </ItemGroup>
 

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml

@@ -75,7 +75,6 @@
                   MinHeight="{TemplateBinding MinHeight}"
                   TemplatedControl.IsTemplateFocusTarget="True">
             <Grid Name="PART_Header"
-                  Background="Transparent"
                   ColumnDefinitions="Auto, *"
                   Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
               <Panel Name="PART_ExpandCollapseChevronContainer"
@@ -88,6 +87,7 @@
               <ContentPresenter Name="PART_HeaderPresenter"
                                 Grid.Column="1"
                                 Focusable="False"
+                                Background="Transparent"
                                 Content="{TemplateBinding Header}"
                                 ContentTemplate="{TemplateBinding HeaderTemplate}"
                                 HorizontalAlignment="{TemplateBinding HorizontalAlignment}"

+ 2 - 1
src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml

@@ -44,18 +44,19 @@
                   Focusable="True"
                   TemplatedControl.IsTemplateFocusTarget="True">
             <Grid Name="PART_Header"
-                  Background="Transparent"
                   Margin="{TemplateBinding Level,
                                            Mode=OneWay,
                                            Converter={StaticResource LeftMarginConverter}}"
                   ColumnDefinitions="16, *">
               <ToggleButton Name="PART_ExpandCollapseChevron"
                             Focusable="False"
+                            Background="Transparent"
                             IsChecked="{TemplateBinding IsExpanded,
                                                         Mode=TwoWay}"
                             Theme="{StaticResource SimpleTreeViewItemToggleButtonTheme}" />
               <ContentPresenter Name="PART_HeaderPresenter"
                                 Grid.Column="1"
+                                Background="Transparent"
                                 Padding="{TemplateBinding Padding}"
                                 HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
                                 Content="{TemplateBinding Header}"

+ 14 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@@ -337,6 +337,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                     var separators = new[] { "," };
                     var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption;
 
+                    var attribute = type.GetAllCustomAttributes().FirstOrDefault(a => a.Type == types.AvaloniaListAttribute);
+                    if (attribute is not null)
+                    {
+                        if (attribute.Properties.TryGetValue("Separators", out var separatorsArray))
+                        {
+                            separators = ((Array)separatorsArray)?.OfType<string>().ToArray();
+                        }
+
+                        if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj))
+                        {
+                            splitOptions = (StringSplitOptions)splitOptionsObj;
+                        }
+                    }
+
                     items = text.Split(separators, splitOptions ^ trimOption);
                     // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested.
                     if (splitOptions.HasFlag(trimOption))

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType InheritDataTypeFromItemsAttribute { get; }
         public IXamlType MarkupExtensionOptionAttribute { get; }
         public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
+        public IXamlType AvaloniaListAttribute { get; }
         public IXamlType AvaloniaList { get; }
         public IXamlType OnExtensionType { get; }
         public IXamlType UnsetValueType { get; }
@@ -143,6 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute");
             MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
             MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
+            AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute");
             AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1");
             OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");
             AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject,

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393
+Subproject commit e5254eb1b2017f78a92acd466c8fa1e47401056b

+ 65 - 0
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@@ -261,6 +261,71 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Can_Move_Forward_Back_Forward()
+        {
+            using var app = Start();
+            var items = new[] { "foo", "bar" };
+            var target = new Carousel
+            {
+                Template = CarouselTemplate(),
+                ItemsSource = items,
+            };
+
+            Prepare(target);
+
+            target.SelectedIndex = 1;
+            Layout(target);
+
+            Assert.Equal(1, target.SelectedIndex);
+
+            target.SelectedIndex = 0;
+            Layout(target);
+
+            Assert.Equal(0, target.SelectedIndex);
+
+            target.SelectedIndex = 1;
+            Layout(target);
+
+            Assert.Equal(1, target.SelectedIndex);
+        }
+
+        [Fact]
+        public void Can_Move_Forward_Back_Forward_With_Control_Items()
+        {
+            // Issue #11119
+            using var app = Start();
+            var items = new[] { new Canvas(), new Canvas() };
+            var target = new Carousel
+            {
+                Template = CarouselTemplate(),
+                ItemsSource = items,
+            };
+
+            Prepare(target);
+
+            target.SelectedIndex = 1;
+            Layout(target);
+
+            Assert.Equal(1, target.SelectedIndex);
+
+            target.SelectedIndex = 0;
+            Layout(target);
+
+            Assert.Equal(0, target.SelectedIndex);
+
+            target.SelectedIndex = 1;
+            target.PropertyChanged += (s, e) =>
+            {
+                if (e.Property == Carousel.SelectedIndexProperty)
+                {
+                }
+            };
+            Layout(target);
+
+            Assert.Equal(1, target.SelectedIndex);
+        }
+
         private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
 
         private static void Prepare(Carousel target)

+ 13 - 0
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -828,6 +828,19 @@ namespace Avalonia.Controls.UnitTests
             Layout(target);
         }
 
+        [Fact]
+        public void ItemIsOwnContainer_Content_Should_Not_Be_Cleared_When_Removed()
+        {
+            // Issue #11128.
+            using var app = Start();
+            var item = new ContentPresenter { Content = "foo" };
+            var target = CreateTarget(items: new[] { item });
+
+            target.Items.RemoveAt(0);
+
+            Assert.Equal("foo", item.Content);
+        }
+
         private static ItemsControl CreateTarget(
             object? dataContext = null,
             IBinding? displayMemberBinding = null,

+ 42 - 7
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -676,12 +676,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
         [Fact]
         public void Moving_Selected_Item_Should_Clear_Selection()
         {
-            var items = new AvaloniaList<Item>
-            {
-                new Item(),
-                new Item(),
-            };
-
+            using var app = Start();
+            var items = new ObservableCollection<string> { "foo", "bar" };
             var target = new SelectingItemsControl
             {
                 ItemsSource = items,
@@ -706,7 +702,46 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.NotNull(receivedArgs);
             Assert.Empty(receivedArgs.AddedItems);
             Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
-            Assert.All(items, x => Assert.False(x.IsSelected));
+        }
+
+        [Fact]
+        public void Moving_Selected_Container_Should_Not_Clear_Selection()
+        {
+            var items = new AvaloniaList<Item>
+            {
+                new Item(),
+                new Item(),
+            };
+
+            var target = new SelectingItemsControl
+            {
+                ItemsSource = items,
+                Template = Template(),
+            };
+
+            Prepare(target);
+            target.SelectedIndex = 1;
+
+            Assert.Equal(items[1], target.SelectedItem);
+            Assert.Equal(1, target.SelectedIndex);
+
+            var receivedArgs = new List<SelectionChangedEventArgs>();
+
+            target.SelectionChanged += (_, args) => receivedArgs.Add(args);
+
+            var moved = items[1];
+            items.Move(1, 0);
+
+            // Because the moved container is still marked as selected on the insert part of the
+            // move, it will remain selected.
+            Assert.Same(moved, target.SelectedItem);
+            Assert.Equal(0, target.SelectedIndex);
+            Assert.NotNull(receivedArgs);
+            Assert.Equal(2, receivedArgs.Count);
+            Assert.Equal(new[] { moved }, receivedArgs[0].RemovedItems);
+            Assert.Equal(new[] { moved }, receivedArgs[1].AddedItems);
+            Assert.True(items[0].IsSelected);
+            Assert.False(items[1].IsSelected);
         }
 
         [Fact]

+ 51 - 1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -1024,6 +1024,56 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal(new[] { 15 }, SelectedContainers(target));
         }
 
+        [Fact]
+        public void Can_Change_Selection_For_Containers_Outside_Of_Viewport()
+        {
+            // Issue #11119
+            using var app = Start();
+            var items = Enumerable.Range(0, 100).Select(x => new TestContainer 
+            { 
+                Content = $"Item {x}",
+                Height = 100,
+            }).ToList();
+
+            // Create a SelectingItemsControl with a virtualizing stack panel.
+            var target = CreateTarget(itemsSource: items, virtualizing: true);
+            target.AutoScrollToSelectedItem = false;
+
+            var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
+            var scroll = panel.FindAncestorOfType<ScrollViewer>()!;
+
+            // Select item 1.
+            target.SelectedIndex = 1;
+
+            // Scroll item 1 and 2 out of view.
+            scroll.Offset = new(0, 1000);
+            Layout(target);
+
+            Assert.Equal(10, panel.FirstRealizedIndex);
+            Assert.Equal(19, panel.LastRealizedIndex);
+
+            // Select item 2 now that items 1 and 2 are both unrealized.
+            target.SelectedIndex = 2;
+
+            // The selection should be updated.
+            Assert.Empty(SelectedContainers(target));
+            Assert.Equal(2, target.SelectedIndex);
+            Assert.Same(items[2], target.SelectedItem);
+            Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes);
+            Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems);
+
+            // Scroll selected item back into view.
+            scroll.Offset = new(0, 0);
+            Layout(target);
+
+            // The selection should be preserved.
+            Assert.Equal(new[] { 2 }, SelectedContainers(target));
+            Assert.Equal(2, target.SelectedIndex);
+            Assert.Same(items[2], target.SelectedItem);
+            Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes);
+            Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems);
+        }
+
         [Fact]
         public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
         {
@@ -1197,7 +1247,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             {
                 Setters =
                 {
-                    new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()),
+                    new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()),
                 },
             };
         }

+ 31 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@@ -261,6 +261,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             var grid = AvaloniaRuntimeXamlLoader.Parse<Grid>(xaml);
 
 
+            Assert.Equal(4, grid.ColumnDefinitions.Count);
+            Assert.Equal(4, grid.RowDefinitions.Count);
+
+            var expected1 = new GridLength(100);
+            var expected2 = GridLength.Auto;
+            var expected3 = new GridLength(1, GridUnitType.Star);
+            var expected4 = new GridLength(100, GridUnitType.Star);
+
+            Assert.Equal(expected1, grid.ColumnDefinitions[0].Width);
+            Assert.Equal(expected2, grid.ColumnDefinitions[1].Width);
+            Assert.Equal(expected3, grid.ColumnDefinitions[2].Width);
+            Assert.Equal(expected4, grid.ColumnDefinitions[3].Width);
+
+            Assert.Equal(expected1, grid.RowDefinitions[0].Height);
+            Assert.Equal(expected2, grid.RowDefinitions[1].Height);
+            Assert.Equal(expected3, grid.RowDefinitions[2].Height);
+            Assert.Equal(expected4, grid.RowDefinitions[3].Height);
+        }
+        
+        [Fact]
+        public void Grid_Row_Col_Definitions_Are_Parsed_Space_Delimiter()
+        {
+            var xaml = @"
+<Grid xmlns='https://github.com/avaloniaui'
+        ColumnDefinitions='100 Auto * 100*'
+        RowDefinitions='100 Auto * 100*'>
+</Grid>";
+
+            var grid = AvaloniaRuntimeXamlLoader.Parse<Grid>(xaml);
+
+
             Assert.Equal(4, grid.ColumnDefinitions.Count);
             Assert.Equal(4, grid.RowDefinitions.Count);