Browse Source

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

pull/11127/head
Steven Kirk 3 years ago
committed by GitHub
parent
commit
3db7059d98
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      packages/Avalonia/Avalonia.csproj
  2. 2
      packages/Avalonia/Avalonia.props
  3. 10
      samples/Sandbox/MainWindow.axaml.cs
  4. 2
      samples/Sandbox/Sandbox.csproj
  5. 13
      src/Avalonia.Base/Input/Cursor.cs
  6. 23
      src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs
  7. 2
      src/Avalonia.Controls/DefinitionList.cs
  8. 28
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  9. 5
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  10. 8
      src/Avalonia.Controls/TopLevel.cs
  11. 6
      src/Avalonia.Controls/TreeViewItem.cs
  12. 9
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  13. 5
      src/Avalonia.Controls/VirtualizingPanel.cs
  14. 14
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  15. 28
      src/Avalonia.Controls/WindowBase.cs
  16. 107
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  17. 14
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  18. 19
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  19. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  20. 2
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  21. 3
      src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml
  22. 14
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  23. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  24. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  25. 65
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  26. 13
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  27. 49
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  28. 52
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  29. 31
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

17
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
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>

10
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
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>

13
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
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
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()

28
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

5
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);

8
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)

6
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;
}

9
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
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>

14
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))

28
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
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
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
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);
}
}
}

2
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>

2
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}"

3
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
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
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,

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

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

65
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
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,

49
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]

52
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
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);

Loading…
Cancel
Save