diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index a5bbcec186..69da211aa4 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
@@ -240,17 +241,14 @@ namespace Avalonia.Controls.Primitives
public override void BeginInit()
{
base.BeginInit();
- ++_updateCount;
- _updateSelectedIndex = int.MinValue;
+
+ InternalBeginInit();
}
///
public override void EndInit()
{
- if (--_updateCount == 0)
- {
- UpdateFinished();
- }
+ InternalEndInit();
base.EndInit();
}
@@ -437,7 +435,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextBeginUpdate();
- ++_updateCount;
+
+ InternalBeginInit();
}
///
@@ -445,10 +444,7 @@ namespace Avalonia.Controls.Primitives
{
base.OnDataContextEndUpdate();
- if (--_updateCount == 0)
- {
- UpdateFinished();
- }
+ InternalEndInit();
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -1118,6 +1114,26 @@ namespace Avalonia.Controls.Primitives
}
}
+ private void InternalBeginInit()
+ {
+ if (_updateCount == 0)
+ {
+ _updateSelectedIndex = int.MinValue;
+ }
+
+ ++_updateCount;
+ }
+
+ private void InternalEndInit()
+ {
+ Debug.Assert(_updateCount > 0);
+
+ if (--_updateCount == 0)
+ {
+ UpdateFinished();
+ }
+ }
+
private class Selection : IEnumerable
{
private readonly List _list = new List();
diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs
index e7cd0697a0..4b3e8e2110 100644
--- a/src/Avalonia.Controls/Primitives/ToggleButton.cs
+++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs
@@ -7,8 +7,14 @@ using Avalonia.Data;
namespace Avalonia.Controls.Primitives
{
+ ///
+ /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states.
+ ///
public class ToggleButton : Button
{
+ ///
+ /// Defines the property.
+ ///
public static readonly DirectProperty IsCheckedProperty =
AvaloniaProperty.RegisterDirect(
nameof(IsChecked),
@@ -17,9 +23,30 @@ namespace Avalonia.Controls.Primitives
unsetValue: null,
defaultBindingMode: BindingMode.TwoWay);
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty IsThreeStateProperty =
AvaloniaProperty.Register(nameof(IsThreeState));
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent CheckedEvent =
+ RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent UncheckedEvent =
+ RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent IndeterminateEvent =
+ RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble);
+
private bool? _isChecked = false;
static ToggleButton()
@@ -27,14 +54,49 @@ namespace Avalonia.Controls.Primitives
PseudoClass(IsCheckedProperty, c => c == true, ":checked");
PseudoClass(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate");
+
+ IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e));
+ }
+
+ ///
+ /// Raised when a is checked.
+ ///
+ public event EventHandler Checked
+ {
+ add => AddHandler(CheckedEvent, value);
+ remove => RemoveHandler(CheckedEvent, value);
+ }
+
+ ///
+ /// Raised when a is unchecked.
+ ///
+ public event EventHandler Unchecked
+ {
+ add => AddHandler(UncheckedEvent, value);
+ remove => RemoveHandler(UncheckedEvent, value);
+ }
+
+ ///
+ /// Raised when a is neither checked nor unchecked.
+ ///
+ public event EventHandler Indeterminate
+ {
+ add => AddHandler(IndeterminateEvent, value);
+ remove => RemoveHandler(IndeterminateEvent, value);
}
+ ///
+ /// Gets or sets whether the is checked.
+ ///
public bool? IsChecked
{
- get { return _isChecked; }
- set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); }
+ get => _isChecked;
+ set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
}
+ ///
+ /// Gets or sets a value that indicates whether the control supports three states.
+ ///
public bool IsThreeState
{
get => GetValue(IsThreeStateProperty);
@@ -47,18 +109,78 @@ namespace Avalonia.Controls.Primitives
base.OnClick();
}
+ ///
+ /// Toggles the property.
+ ///
protected virtual void Toggle()
{
if (IsChecked.HasValue)
+ {
if (IsChecked.Value)
+ {
if (IsThreeState)
+ {
IsChecked = null;
+ }
else
+ {
IsChecked = false;
+ }
+ }
else
+ {
IsChecked = true;
+ }
+ }
else
+ {
IsChecked = false;
+ }
+ }
+
+ ///
+ /// Called when becomes true.
+ ///
+ /// Event arguments for the routed event that is raised by the default implementation of this method.
+ protected virtual void OnChecked(RoutedEventArgs e)
+ {
+ RaiseEvent(e);
+ }
+
+ ///
+ /// Called when becomes false.
+ ///
+ /// Event arguments for the routed event that is raised by the default implementation of this method.
+ protected virtual void OnUnchecked(RoutedEventArgs e)
+ {
+ RaiseEvent(e);
+ }
+
+ ///
+ /// Called when becomes null.
+ ///
+ /// Event arguments for the routed event that is raised by the default implementation of this method.
+ protected virtual void OnIndeterminate(RoutedEventArgs e)
+ {
+ RaiseEvent(e);
+ }
+
+ private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var newValue = (bool?)e.NewValue;
+
+ switch (newValue)
+ {
+ case true:
+ OnChecked(new RoutedEventArgs(CheckedEvent));
+ break;
+ case false:
+ OnUnchecked(new RoutedEventArgs(UncheckedEvent));
+ break;
+ default:
+ OnIndeterminate(new RoutedEventArgs(IndeterminateEvent));
+ break;
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
index 0e2136a6f3..cbac1d6c1b 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -562,33 +562,35 @@ namespace Avalonia.Controls
if (Layout != null)
{
- if (Layout is VirtualizingLayout virtualLayout)
- {
- var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ try
+ {
_processingItemsSourceChange = args;
- try
+ if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
- finally
- {
- _processingItemsSourceChange = null;
- }
- }
- else if (Layout is NonVirtualizingLayout nonVirtualLayout)
- {
- // Walk through all the elements and make sure they are cleared for
- // non-virtualizing layouts.
- foreach (var element in Children)
+ else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
- if (GetVirtualizationInfo(element).IsRealized)
+ // Walk through all the elements and make sure they are cleared for
+ // non-virtualizing layouts.
+ foreach (var element in Children)
{
- ClearElementImpl(element);
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
}
+
+ Children.Clear();
}
}
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
InvalidateMeasure();
}
diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs
index 51c14d47d6..7d005a30b4 100644
--- a/src/Avalonia.Controls/Repeater/ViewManager.cs
+++ b/src/Avalonia.Controls/Repeater/ViewManager.cs
@@ -109,11 +109,22 @@ namespace Avalonia.Controls
public void ClearElementToElementFactory(IControl element)
{
- var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
- var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element);
- _owner.ItemTemplateShim.RecycleElement(_owner, element);
+ if (_owner.ItemTemplateShim != null)
+ {
+ _owner.ItemTemplateShim.RecycleElement(_owner, element);
+ }
+ else
+ {
+ // No ItemTemplate to recycle to, remove the element from the children collection.
+ if (!_owner.Children.Remove(element))
+ {
+ throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection.");
+ }
+ }
+
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element)
@@ -121,9 +132,8 @@ namespace Avalonia.Controls
// Focused element is going away. Remove the tracked last focused element
// and pick a reasonable next focus if we can find one within the layout
// realized elements.
- MoveFocusFromClearedIndex(clearedIndex);
+ MoveFocusFromClearedIndex(virtInfo.Index);
}
-
}
private void MoveFocusFromClearedIndex(int clearedIndex)
@@ -190,7 +200,8 @@ namespace Avalonia.Controls
{
if (virtInfo == null)
{
- throw new ArgumentException("Element is not a child of this ItemsRepeater.");
+ //Element is not a child of this ItemsRepeater.
+ return -1;
}
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
@@ -515,21 +526,52 @@ namespace Avalonia.Controls
return element;
}
+ // There are several cases handled here with respect to which element gets returned and when DataContext is modified.
+ //
+ // 1. If there is no ItemTemplate:
+ // 1.1 If data is an IControl -> the data is returned
+ // 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data
+ //
+ // 2. If there is an ItemTemplate:
+ // 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data
+ // 2.2 If data is an IControl:
+ // 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is
+ // 2.2.2 If Element returned by the ElementFactory is not the same as the data
+ // -> Element that is fetched from the ElementFactory is returned and
+ // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself
private IControl GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
+ var data = _owner.ItemsSourceView.GetAt(index);
+ var providedElementFactory = _owner.ItemTemplateShim;
+
+ ItemTemplateWrapper GetElementFactory()
+ {
+ if (providedElementFactory == null)
+ {
+ var factory = FuncDataTemplate.Default;
+ _owner.ItemTemplate = factory;
+ return _owner.ItemTemplateShim;
+ }
- var itemTemplateFactory = _owner.ItemTemplateShim;
- if (itemTemplateFactory == null)
+ return providedElementFactory;
+ }
+
+ IControl GetElement()
{
- // If no ItemTemplate was provided, use a default
- var factory = FuncDataTemplate.Default;
- _owner.ItemTemplate = factory;
- itemTemplateFactory = _owner.ItemTemplateShim;
+ if (providedElementFactory == null)
+ {
+ if (data is IControl dataAsElement)
+ {
+ return dataAsElement;
+ }
+ }
+
+ var elementFactory = GetElementFactory();
+ return elementFactory.GetElement(_owner, data);
}
- var data = _owner.ItemsSourceView.GetAt(index);
- var element = itemTemplateFactory.GetElement(_owner, data);
+ var element = GetElement();
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null)
@@ -537,8 +579,11 @@ namespace Avalonia.Controls
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
- // Prepare the element
- element.DataContext = data;
+ if (data != element)
+ {
+ // Prepare the element
+ element.DataContext = data;
+ }
virtInfo.MoveOwnershipToLayoutFromElementFactory(
index,
diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
index 615ce725bd..7f44c80a64 100644
--- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
+++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
@@ -72,6 +72,7 @@ namespace Avalonia.Layout
bool isWrapping,
double minItemSpacing,
double lineSpacing,
+ int maxItemsPerLine,
ScrollOrientation orientation,
string layoutId)
{
@@ -94,14 +95,14 @@ namespace Avalonia.Layout
_elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
- Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
- Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+ Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
+ Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
if (isWrapping && IsReflowRequired())
{
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
- Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
+ Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
}
RaiseLineArranged();
@@ -115,10 +116,11 @@ namespace Avalonia.Layout
public Size Arrange(
Size finalSize,
VirtualizingLayoutContext context,
+ bool isWrapping,
LineAlignment lineAlignment,
string layoutId)
{
- ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId);
+ ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId);
return new Size(
Math.Max(finalSize.Width, _lastExtent.Width),
@@ -270,6 +272,7 @@ namespace Avalonia.Layout
Size availableSize,
double minItemSpacing,
double lineSpacing,
+ int maxItemsPerLine,
string layoutId)
{
if (anchorIndex != -1)
@@ -280,7 +283,7 @@ namespace Avalonia.Layout
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
var lineOffset = _orientation.MajorStart(anchorBounds);
var lineMajorSize = _orientation.MajorSize(anchorBounds);
- int countInLine = 1;
+ var countInLine = 1;
int count = 0;
bool lineNeedsReposition = false;
@@ -301,7 +304,7 @@ namespace Avalonia.Layout
if (direction == GenerateDirection.Forward)
{
double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
- if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// No more space in this row. wrap to next row.
_orientation.SetMinorStart(ref currentBounds, 0);
@@ -339,7 +342,7 @@ namespace Avalonia.Layout
{
// Backward
double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
- if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// Does not fit, wrap to the previous row
var availableSizeMinor = _orientation.Minor(availableSize);
@@ -544,6 +547,7 @@ namespace Avalonia.Layout
private void ArrangeVirtualizingLayout(
Size finalSize,
LineAlignment lineAlignment,
+ bool isWrapping,
string layoutId)
{
// Walk through the realized elements one line at a time and
@@ -563,7 +567,7 @@ namespace Avalonia.Layout
if (_orientation.MajorStart(currentBounds) != currentLineOffset)
{
spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
- PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId);
+ PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
spaceAtLineStart = _orientation.MinorStart(currentBounds);
countInLine = 0;
currentLineOffset = _orientation.MajorStart(currentBounds);
@@ -580,7 +584,7 @@ namespace Avalonia.Layout
if (countInLine > 0)
{
var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
- PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId);
+ PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
}
}
}
@@ -594,6 +598,8 @@ namespace Avalonia.Layout
double spaceAtLineEnd,
double lineSize,
LineAlignment lineAlignment,
+ bool isWrapping,
+ Size finalSize,
string layoutId)
{
for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
@@ -659,6 +665,14 @@ namespace Avalonia.Layout
}
bounds = bounds.Translate(-_lastExtent.Position);
+
+ if (!isWrapping)
+ {
+ _orientation.SetMinorSize(
+ ref bounds,
+ Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize)));
+ }
+
var element = _elementManager.GetAt(rangeIndex);
element.Arrange(bounds);
}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs
index fba91e66c7..5d27ba9199 100644
--- a/src/Avalonia.Layout/NonVirtualizingLayout.cs
+++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs
@@ -20,25 +20,25 @@ namespace Avalonia.Layout
///
public sealed override void InitializeForContext(LayoutContext context)
{
- InitializeForContextCore((VirtualizingLayoutContext)context);
+ InitializeForContextCore((NonVirtualizingLayoutContext)context);
}
///
public sealed override void UninitializeForContext(LayoutContext context)
{
- UninitializeForContextCore((VirtualizingLayoutContext)context);
+ UninitializeForContextCore((NonVirtualizingLayoutContext)context);
}
///
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
- return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+ return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
}
///
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
- return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+ return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
}
///
@@ -49,7 +49,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ protected virtual void InitializeForContextCore(LayoutContext context)
{
}
@@ -61,7 +61,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ protected virtual void UninitializeForContextCore(LayoutContext context)
{
}
@@ -83,7 +83,7 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
///
- protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+ protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
///
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +98,6 @@ namespace Avalonia.Layout
/// its children.
///
/// The actual size that is used after the element is arranged in layout.
- protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+ protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
new file mode 100644
index 0000000000..d3dec83e9b
--- /dev/null
+++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
@@ -0,0 +1,14 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for layout context types that do not support virtualization.
+ ///
+ public abstract class NonVirtualizingLayoutContext : LayoutContext
+ {
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
index e9735b9b31..3c3729272c 100644
--- a/src/Avalonia.Layout/StackLayout.cs
+++ b/src/Avalonia.Layout/StackLayout.cs
@@ -267,6 +267,7 @@ namespace Avalonia.Layout
false,
0,
Spacing,
+ int.MaxValue,
_orientation.ScrollOrientation,
LayoutId);
@@ -278,6 +279,7 @@ namespace Avalonia.Layout
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
+ false,
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
index edc2042922..11a521ed1e 100644
--- a/src/Avalonia.Layout/UniformGridLayout.cs
+++ b/src/Avalonia.Layout/UniformGridLayout.cs
@@ -110,6 +110,12 @@ namespace Avalonia.Layout
public static readonly StyledProperty MinRowSpacingProperty =
AvaloniaProperty.Register(nameof(MinRowSpacing));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaximumRowsOrColumnsProperty =
+ AvaloniaProperty.Register(nameof(MinItemWidth));
+
///
/// Defines the property.
///
@@ -123,6 +129,7 @@ namespace Avalonia.Layout
private double _minColumnSpacing;
private UniformGridLayoutItemsJustification _itemsJustification;
private UniformGridLayoutItemsStretch _itemsStretch;
+ private int _maximumRowsOrColumns = int.MaxValue;
///
/// Initializes a new instance of the class.
@@ -219,6 +226,15 @@ namespace Avalonia.Layout
set => SetValue(MinRowSpacingProperty, value);
}
+ ///
+ /// Gets or sets the maximum row or column count.
+ ///
+ public int MaximumRowsOrColumns
+ {
+ get => GetValue(MaximumRowsOrColumnsProperty);
+ set => SetValue(MaximumRowsOrColumnsProperty, value);
+ }
+
///
/// Gets or sets the axis along which items are laid out.
///
@@ -269,15 +285,17 @@ namespace Avalonia.Layout
{
var gridState = (UniformGridLayoutState)context.LayoutState;
var lastExtent = gridState.FlowAlgorithm.LastExtent;
- int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
- double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
- double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+ var itemsPerLine = Math.Min( // note use of unsigned ints
+ Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+ Math.Max(1u, (uint)_maximumRowsOrColumns));
+ var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
+ var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
{
double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
- anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
+ anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
}
}
@@ -299,7 +317,9 @@ namespace Avalonia.Layout
int count = context.ItemCount;
if (targetIndex >= 0 && targetIndex < count)
{
- int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int itemsPerLine = (int)Math.Min( // note use of unsigned ints
+ Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+ Math.Max(1u, _maximumRowsOrColumns));
int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = indexOfFirstInLine;
var state = context.LayoutState as UniformGridLayoutState;
@@ -329,17 +349,21 @@ namespace Avalonia.Layout
// Constants
int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize);
- int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
- (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
+ int itemsPerLine =
+ (int)Math.Min( // note use of unsigned ints
+ Math.Max(1u, !double.IsInfinity(availableSizeMinor)
+ ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context))
+ : (uint)itemsCount),
+ Math.Max(1u, _maximumRowsOrColumns));
double lineSize = GetMajorSizeWithSpacing(context);
if (itemsCount > 0)
{
_orientation.SetMinorSize(
ref extent,
- !double.IsInfinity(availableSizeMinor) ?
+ !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ?
availableSizeMinor :
- Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
+ Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
_orientation.SetMajorSize(
ref extent,
Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
@@ -398,7 +422,7 @@ namespace Avalonia.Layout
// Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
var gridState = (UniformGridLayoutState)context.LayoutState;
- gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing);
+ gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns);
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
@@ -406,6 +430,7 @@ namespace Avalonia.Layout
true,
MinItemSpacing,
LineSpacing,
+ _maximumRowsOrColumns,
_orientation.ScrollOrientation,
LayoutId);
@@ -421,6 +446,7 @@ namespace Avalonia.Layout
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
+ true,
(FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
LayoutId);
return new Size(value.Width, value.Height);
@@ -471,6 +497,10 @@ namespace Avalonia.Layout
{
_minItemHeight = (double)args.NewValue;
}
+ else if (args.Property == MaximumRowsOrColumnsProperty)
+ {
+ _maximumRowsOrColumns = (int)args.NewValue;
+ }
InvalidateLayout();
}
@@ -499,7 +529,9 @@ namespace Avalonia.Layout
Rect lastExtent,
VirtualizingLayoutContext context)
{
- int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int itemsPerLine = (int)Math.Min( //note use of unsigned ints
+ Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+ Math.Max(1u, _maximumRowsOrColumns));
int rowIndex = (int)(index / itemsPerLine);
int indexInRow = index - (rowIndex * itemsPerLine);
diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs
index e6d75bcf35..62c5174775 100644
--- a/src/Avalonia.Layout/UniformGridLayoutState.cs
+++ b/src/Avalonia.Layout/UniformGridLayoutState.cs
@@ -48,8 +48,14 @@ namespace Avalonia.Layout
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
- double minColumnSpacing)
+ double minColumnSpacing,
+ int maxItemsPerLine)
{
+ if (maxItemsPerLine == 0)
+ {
+ maxItemsPerLine = 1;
+ }
+
if (context.ItemCount > 0)
{
// If the first element is realized we don't need to cache it or to get it from the context
@@ -57,7 +63,7 @@ namespace Avalonia.Layout
if (realizedElement != null)
{
realizedElement.Measure(availableSize);
- SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+ SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
_cachedFirstElement = null;
}
else
@@ -72,7 +78,7 @@ namespace Avalonia.Layout
_cachedFirstElement.Measure(availableSize);
- SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+ SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
// See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
@@ -92,8 +98,14 @@ namespace Avalonia.Layout
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
- double minColumnSpacing)
+ double minColumnSpacing,
+ int maxItemsPerLine)
{
+ if (maxItemsPerLine == 0)
+ {
+ maxItemsPerLine = 1;
+ }
+
EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
@@ -101,11 +113,17 @@ namespace Avalonia.Layout
var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;
var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight;
- itemSizeMinor += minorItemSpacing;
- var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor));
- var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor);
- var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn;
+ double extraMinorPixelsForEachItem = 0.0;
+ if (!double.IsInfinity(availableSizeMinor))
+ {
+ var numItemsPerColumn = Math.Min(
+ maxItemsPerLine,
+ Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing)));
+ var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing;
+ var remainingSpace = ((int)(availableSizeMinor - usedSpace));
+ extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn);
+ }
if (stretch == UniformGridLayoutItemsStretch.Fill)
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
index d819581000..696c0dbf46 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
@@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(-1, target.SelectedIndex);
}
+ [Fact]
+ public void SelectedIndex_Should_Be_Minus_1_Without_Initialize()
+ {
+ var items = new[]
+ {
+ new Item(),
+ new Item(),
+ };
+
+ var target = new ListBox();
+ target.Items = items;
+ target.Template = Template();
+ target.DataContext = new object();
+
+ Assert.Equal(-1, target.SelectedIndex);
+ }
+
[Fact]
public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected()
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs
index 4f4ab47b0a..9acd42aba6 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs
@@ -1,5 +1,4 @@
using Avalonia.Data;
-using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests
Assert.Null(threeStateButton.IsChecked);
}
+ [Fact]
+ public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes()
+ {
+ var threeStateButton = new ToggleButton();
+
+ bool checkedRaised = false;
+ threeStateButton.Checked += (_, __) => checkedRaised = true;
+
+ threeStateButton.IsChecked = true;
+ Assert.True(checkedRaised);
+
+ bool uncheckedRaised = false;
+ threeStateButton.Unchecked += (_, __) => uncheckedRaised = true;
+
+ threeStateButton.IsChecked = false;
+ Assert.True(uncheckedRaised);
+
+ bool indeterminateRaised = false;
+ threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true;
+
+ threeStateButton.IsChecked = null;
+ Assert.True(indeterminateRaised);
+ }
+
+ [Fact]
+ public void ToggleButton_Events_Are_Raised_When_Toggling()
+ {
+ var threeStateButton = new TestToggleButton { IsThreeState = true };
+
+ bool checkedRaised = false;
+ threeStateButton.Checked += (_, __) => checkedRaised = true;
+
+ threeStateButton.Toggle();
+ Assert.True(checkedRaised);
+
+ bool indeterminateRaised = false;
+ threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true;
+
+ threeStateButton.Toggle();
+ Assert.True(indeterminateRaised);
+
+ bool uncheckedRaised = false;
+ threeStateButton.Unchecked += (_, __) => uncheckedRaised = true;
+
+ threeStateButton.Toggle();
+ Assert.True(uncheckedRaised);
+ }
+
private class Class1 : NotifyingBase
{
private bool _foo;
@@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests
set { nullableFoo = value; RaisePropertyChanged(); }
}
}
+
+ private class TestToggleButton : ToggleButton
+ {
+ public new void Toggle() => base.Toggle();
+ }
}
}