Browse Source

Merge remote-tracking branch 'origin/master' into include-analyzers-in-the-main-package

pull/11029/head
Max Katz 3 years ago
parent
commit
32937f6dda
  1. 10
      samples/Sandbox/MainWindow.axaml.cs
  2. 2
      samples/Sandbox/Sandbox.csproj
  3. 12
      src/Avalonia.Base/Animation/Animation.cs
  4. 10
      src/Avalonia.Base/Layout/LayoutManager.cs
  5. 18
      src/Avalonia.Base/Layout/LayoutQueue.cs
  6. 40
      src/Avalonia.Base/Layout/Layoutable.cs
  7. 23
      src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs
  8. 2
      src/Avalonia.Controls/DefinitionList.cs
  9. 43
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  10. 9
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  11. 5
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  12. 2
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  13. 8
      src/Avalonia.Controls/TopLevel.cs
  14. 6
      src/Avalonia.Controls/TreeViewItem.cs
  15. 28
      src/Avalonia.Controls/Utils/RealizedStackElements.cs
  16. 9
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  17. 5
      src/Avalonia.Controls/VirtualizingPanel.cs
  18. 48
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  19. 28
      src/Avalonia.Controls/WindowBase.cs
  20. 28
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  21. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  22. 2
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  23. 3
      src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml
  24. 14
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  25. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  26. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  27. 56
      tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
  28. 32
      tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs
  29. 65
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  30. 5
      tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
  31. 13
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  32. 49
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  33. 87
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  34. 31
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

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>

12
src/Avalonia.Base/Animation/Animation.cs

@ -200,7 +200,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter,
public static void SetAnimator(IAnimationSetter setter,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)]
Type value)
{
@ -319,7 +319,7 @@ namespace Avalonia.Animation
if (animators.Count == 1)
{
var subscription = animators[0].Apply(this, control, clock, match, onComplete);
if (subscription is not null)
{
subscriptions.Add(subscription);
@ -348,9 +348,11 @@ namespace Avalonia.Animation
if (onComplete != null)
{
Task.WhenAll(completionTasks!).ContinueWith(
(_, state) => ((Action)state!).Invoke(),
onComplete);
Task.WhenAll(completionTasks!)
.ContinueWith((_, state) => ((Action)state!).Invoke()
, onComplete
, TaskScheduler.FromCurrentSynchronizationContext()
);
}
}
return new CompositeDisposable(subscriptions);

10
src/Avalonia.Base/Layout/LayoutManager.cs

@ -17,7 +17,7 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager, IDisposable
{
private const int MaxPasses = 3;
private const int MaxPasses = 10;
private readonly Layoutable _owner;
private readonly LayoutQueue<Layoutable> _toMeasure = new LayoutQueue<Layoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<Layoutable> _toArrange = new LayoutQueue<Layoutable>(v => !v.IsArrangeValid);
@ -249,10 +249,12 @@ namespace Avalonia.Layout
{
var control = _toMeasure.Dequeue();
if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
if (!control.IsMeasureValid)
{
Measure(control);
}
_toArrange.Enqueue(control);
}
}
@ -262,7 +264,7 @@ namespace Avalonia.Layout
{
var control = _toArrange.Dequeue();
if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
if (!control.IsArrangeValid)
{
Arrange(control);
}
@ -297,8 +299,6 @@ namespace Avalonia.Layout
{
control.Measure(control.PreviousMeasure.Value);
}
_toArrange.Enqueue(control);
}
return true;

18
src/Avalonia.Base/Layout/LayoutQueue.cs

@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Logging;
namespace Avalonia.Layout
{
@ -48,10 +49,21 @@ namespace Avalonia.Layout
{
_loopQueueInfo.TryGetValue(item, out var info);
if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
if (!info.Active)
{
_inner.Enqueue(item);
_loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
if (info.Count < _maxEnqueueCountPerLoop)
{
_inner.Enqueue(item);
_loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
}
else
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log(
this,
"Layout cycle detected. Item {Item} was enqueued {Count} times.",
item,
info.Count);
}
}
}

40
src/Avalonia.Base/Layout/Layoutable.cs

@ -776,10 +776,24 @@ namespace Avalonia.Layout
// All changes to visibility cause the parent element to be notified.
this.GetVisualParent<Layoutable>()?.ChildDesiredSizeChanged(this);
// We only invalidate outselves when visibility is changed to true.
if (change.GetNewValue<bool>())
{
// We only invalidate ourselves when visibility is changed to true.
InvalidateMeasure();
// If any descendant had its measure/arrange invalidated while we were hidden,
// they will need to to be registered with the layout manager now that they
// are again effectively visible. If IsEffectivelyVisible becomes an observable
// property then we can piggy-pack on that; for the moment we do this manually.
if (VisualRoot is ILayoutRoot layoutRoot)
{
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
(VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager);
}
}
}
}
}
@ -804,6 +818,30 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
private void AncestorBecameVisible(ILayoutManager layoutManager)
{
if (!IsVisible)
return;
if (!IsMeasureValid)
{
layoutManager.InvalidateMeasure(this);
InvalidateVisual();
}
else if (!IsArrangeValid)
{
layoutManager.InvalidateArrange(this);
InvalidateVisual();
}
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
(VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager);
}
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>

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

43
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
@ -65,7 +85,7 @@ namespace Avalonia.Controls.Generators
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// only needs to be called a single time, otherwise this method should be called after the
/// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// </remarks>
@ -80,10 +100,11 @@ namespace Avalonia.Controls.Generators
/// <param name="item">The item being displayed.</param>
/// <param name="index">The index of the item being displayed.</param>
/// <remarks>
/// This method should be called when a container has been fully prepared and added
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It should be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/>.
/// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
/// must be called only a single time.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -102,6 +123,12 @@ namespace Avalonia.Controls.Generators
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
/// </summary>
/// <param name="container">The container control.</param>
/// <remarks>
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
[Obsolete("Use ItemsControl.ContainerFromIndex")]

9
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -1,5 +1,5 @@
using System;
using Avalonia.Collections;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
@ -442,7 +442,7 @@ namespace Avalonia.Controls.Presenters
var contentTemplate = ContentTemplate;
var oldChild = Child;
var newChild = CreateChild(content, oldChild, contentTemplate);
var logicalChildren = Host?.LogicalChildren ?? LogicalChildren;
var logicalChildren = GetEffectiveLogicalChildren();
// Remove the old child if we're not recycling it.
if (newChild != oldChild)
@ -488,6 +488,9 @@ namespace Avalonia.Controls.Presenters
}
private IAvaloniaList<ILogical> GetEffectiveLogicalChildren()
=> Host?.LogicalChildren ?? LogicalChildren;
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
@ -692,7 +695,7 @@ namespace Avalonia.Controls.Presenters
else if (Child != null)
{
VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
GetEffectiveLogicalChildren().Remove(Child);
((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
_recyclingDataTemplate = null;

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

2
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -100,7 +100,7 @@ namespace Avalonia.Controls
}
}
public PullDirection PullDirection
internal PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);

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

28
src/Avalonia.Controls/Utils/RealizedStackElements.cs

@ -353,7 +353,10 @@ namespace Avalonia.Controls.Utils
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
{
_elements[i] = null;
recycleElement(element);
}
}
_elements.RemoveRange(start, end - start);
@ -389,10 +392,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
foreach (var e in _elements)
for (var i = 0; i < _elements.Count; i++)
{
if (e is not null)
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e);
}
}
_startU = _firstIndex = 0;
@ -422,7 +428,10 @@ namespace Avalonia.Controls.Utils
for (var i = 0; i < endIndex; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(0, endIndex);
@ -453,7 +462,10 @@ namespace Avalonia.Controls.Utils
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(startIndex, _elements.Count - startIndex);
@ -470,13 +482,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
var i = FirstIndex;
foreach (var e in _elements)
for (var i = 0; i < _elements.Count; i++)
{
if (e is not null)
recycleElement(e, i);
++i;
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_startU = _firstIndex = 0;

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>

48
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)
@ -556,7 +565,6 @@ namespace Avalonia.Controls
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
InvalidateHack(e);
return e;
}
@ -578,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))
@ -705,39 +712,6 @@ namespace Avalonia.Controls
}
}
private static void InvalidateHack(Control c)
{
bool HasInvalidations(Control c)
{
if (!c.IsMeasureValid)
return true;
for (var i = 0; i < c.VisualChildren.Count; ++i)
{
if (c.VisualChildren[i] is Control child)
{
if (!child.IsMeasureValid || HasInvalidations(child))
return true;
}
}
return false;
}
void Invalidate(Control c)
{
c.InvalidateMeasure();
for (var i = 0; i < c.VisualChildren.Count; ++i)
{
if (c.VisualChildren[i] is Control child)
Invalidate(child);
}
}
if (HasInvalidations(c))
Invalidate(c);
}
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
{
if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)

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>

28
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -39,16 +39,6 @@ namespace Avalonia.Diagnostics.Views
AdornerLayer.SetIsClipEnabled(_adorner, false);
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
((TreePageViewModel)DataContext!).ClipboardCopyRequested += (sender, s) =>
{
TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(s);
};
}
protected void AddAdorner(object? sender, PointerEventArgs e)
{
var node = (TreeNode?)((Control)sender!).DataContext;
@ -108,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

56
tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs

@ -59,6 +59,29 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.False(control.Arranged);
}
[Fact]
public void Lays_Out_Descendents_That_Were_Invalidated_While_Ancestor_Was_Not_Visible()
{
// Issue #11076
var control = new LayoutTestControl();
var parent = new Decorator { Child = control };
var grandparent = new Decorator { Child = parent };
var root = new LayoutTestRoot { Child = grandparent };
root.LayoutManager.ExecuteInitialLayoutPass();
grandparent.IsVisible = false;
control.InvalidateMeasure();
root.LayoutManager.ExecuteInitialLayoutPass();
grandparent.IsVisible = true;
root.LayoutManager.ExecuteLayoutPass();
Assert.True(control.IsMeasureValid);
Assert.True(control.IsArrangeValid);
}
[Fact]
public void Arranges_InvalidateArranged_Control()
{
@ -491,5 +514,38 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.True(parent.IsMeasureValid);
Assert.True(parent.IsArrangeValid);
}
[Fact]
public void Grandparent_Can_Invalidate_Root_Measure_During_Arrange()
{
// Issue #11161.
var child = new LayoutTestControl();
var parent = new LayoutTestControl { Child = child };
var grandparent = new LayoutTestControl { Child = parent };
var root = new LayoutTestRoot { Child = grandparent };
root.LayoutManager.ExecuteInitialLayoutPass();
grandparent.DoArrangeOverride = (_, s) =>
{
root.InvalidateMeasure();
return s;
};
grandparent.CallBaseArrange = true;
child.InvalidateMeasure();
grandparent.InvalidateMeasure();
root.LayoutManager.ExecuteLayoutPass();
Assert.True(child.IsMeasureValid);
Assert.True(child.IsArrangeValid);
Assert.True(parent.IsMeasureValid);
Assert.True(parent.IsArrangeValid);
Assert.True(grandparent.IsMeasureValid);
Assert.True(grandparent.IsArrangeValid);
Assert.True(root.IsMeasureValid);
Assert.True(root.IsArrangeValid);
}
}
}

32
tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs

@ -10,21 +10,41 @@ namespace Avalonia.Base.UnitTests.Layout
public bool Arranged { get; set; }
public Func<Layoutable, Size, Size> DoMeasureOverride { get; set; }
public Func<Layoutable, Size, Size> DoArrangeOverride { get; set; }
public bool CallBaseMeasure { get; set; }
public bool CallBaseArrange { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
Measured = true;
return DoMeasureOverride != null ?
DoMeasureOverride(this, availableSize) :
base.MeasureOverride(availableSize);
if (DoMeasureOverride is not null)
{
var overrideResult = DoMeasureOverride(this, availableSize);
return CallBaseMeasure ?
base.MeasureOverride(overrideResult) :
overrideResult;
}
else
{
return base.MeasureOverride(availableSize);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
Arranged = true;
return DoArrangeOverride != null ?
DoArrangeOverride(this, finalSize) :
base.ArrangeOverride(finalSize);
if (DoArrangeOverride is not null)
{
var overrideResult = DoArrangeOverride(this, finalSize);
return CallBaseArrange ?
base.ArrangeOverride(overrideResult) :
overrideResult;
}
else
{
return base.ArrangeOverride(finalSize);
}
}
}
}

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)

5
tests/Avalonia.Controls.UnitTests/ContentControlTests.cs

@ -359,16 +359,21 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
Assert.Equal(target, target.Presenter.Child.GetLogicalParent());
Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren);
root.Child = null;
Assert.Null(target.Template);
target.Content = null;
Assert.Empty(target.LogicalChildren);
root.Child = target;
target.Content = "Bar";
Assert.Equal(target, target.Presenter.Child.GetLogicalParent());
Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren);
}
private static FuncControlTemplate GetTemplate()

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]

87
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1024,6 +1024,90 @@ 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_Is_Not_Cleared_On_Recycling_Containers()
{
using var app = Start();
var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList();
// Create a SelectingItemsControl that creates containers that raise IsSelectedChanged,
// with a virtualizing stack panel.
var target = CreateTarget<TestSelectorWithContainers>(
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 out of view.
scroll.Offset = new(0, 1000);
Layout(target);
Assert.Equal(10, panel.FirstRealizedIndex);
Assert.Equal(19, panel.LastRealizedIndex);
// The selection should be preserved.
Assert.Empty(SelectedContainers(target));
Assert.Equal(1, target.SelectedIndex);
Assert.Same(items[1], target.SelectedItem);
Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);
Assert.Equal(new[] { items[1] }, target.Selection.SelectedItems);
}
[Fact]
public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
{
@ -1197,7 +1281,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Setters =
{
new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()),
new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()),
new Setter(TestContainer.HeightProperty, 100.0),
},
};
}

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