diff --git a/Documentation/build.md b/Documentation/build.md
index 56b028206d..8c2ef57b54 100644
--- a/Documentation/build.md
+++ b/Documentation/build.md
@@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS:
```
-brew install castxml
+brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
```
On Debian based Linux (Debian, Ubuntu, Mint, etc):
diff --git a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs
index 0e0b3d6142..cc847a594d 100644
--- a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs
+++ b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs
@@ -17,7 +17,7 @@ namespace RenderDemo.Controls
public LineBoundsDemoControl()
{
var timer = new DispatcherTimer();
- timer.Interval = TimeSpan.FromSeconds(1 / 60);
+ timer.Interval = TimeSpan.FromSeconds(1 / 60.0);
timer.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start();
}
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 1735599988..86499530da 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
@@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
+using Avalonia.Styling;
namespace Avalonia.Controls
{
///
/// A control context menu.
///
- public class ContextMenu : MenuBase
+ public class ContextMenu : MenuBase, ISetterValue
{
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
- private Control _attachedControl;
+ private List _attachedControls;
private IInputElement _previousFocus;
///
@@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
- oldMenu._attachedControl = null;
+ oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
- newMenu._attachedControl = control;
+ newMenu._attachedControls ??= new List();
+ newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
}
}
@@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// The control.
public void Open(Control control)
{
- if (control is null && _attachedControl is null)
+ if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
- if (control is object && _attachedControl is object && control != _attachedControl)
+ if (control is object &&
+ _attachedControls is object &&
+ !_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
+ control ??= _attachedControls[0];
+
if (IsOpen)
{
return;
@@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
- ((ISetLogicalParent)_popup).SetParent(control);
+ if (_popup.Parent != control)
+ {
+ ((ISetLogicalParent)_popup).SetParent(null);
+ ((ISetLogicalParent)_popup).SetParent(control);
+ }
+
_popup.Child = this;
_popup.IsOpen = true;
@@ -155,6 +167,17 @@ namespace Avalonia.Controls
}
}
+ void ISetterValue.Initialize(ISetter setter)
+ {
+ // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
+ // the behavior defined in Control which requires controls to be wrapped in a .
+ if (!(setter is Setter s && s.Property == ContextMenuProperty))
+ {
+ throw new InvalidOperationException(
+ "Cannot use a control as a Setter value. Wrap the control in a .");
+ }
+ }
+
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1;
IsOpen = false;
- if (_attachedControl is null)
+ if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
index 086599d0bb..12fba12c8c 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -379,14 +380,19 @@ namespace Avalonia.Controls
{
if (property == ItemsProperty)
{
+ var oldEnumerable = oldValue.GetValueOrDefault();
var newEnumerable = newValue.GetValueOrDefault();
- var newDataSource = newEnumerable as ItemsSourceView;
- if (newEnumerable != null && newDataSource == null)
+
+ if (oldEnumerable != newEnumerable)
{
- newDataSource = new ItemsSourceView(newEnumerable);
- }
+ var newDataSource = newEnumerable as ItemsSourceView;
+ if (newEnumerable != null && newDataSource == null)
+ {
+ newDataSource = new ItemsSourceView(newEnumerable);
+ }
- OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
+ OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
+ }
}
else if (property == ItemTemplateProperty)
{
@@ -431,8 +437,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element)
{
- var virtInfo = TryGetVirtualizationInfo(element);
- return _viewManager.GetElementIndex(virtInfo);
+ // Verify that element is actually a child of this ItemsRepeater
+ var parent = element.GetVisualParent();
+
+ if (parent == this)
+ {
+ var virtInfo = TryGetVirtualizationInfo(element);
+ return _viewManager.GetElementIndex(virtInfo);
+ }
+
+ return -1;
}
private IControl GetElementFromIndexImpl(int index)
diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs
index 7d005a30b4..4ed9cb3333 100644
--- a/src/Avalonia.Controls/Repeater/ViewManager.cs
+++ b/src/Avalonia.Controls/Repeater/ViewManager.cs
@@ -388,19 +388,24 @@ namespace Avalonia.Controls
}
case NotifyCollectionChangedAction.Reset:
- if (_owner.ItemsSourceView.HasKeyIndexMapping)
+ // If we get multiple resets back to back before
+ // running layout, we dont have to clear all the elements again.
+ if (!_isDataSourceStableResetPending)
{
- _isDataSourceStableResetPending = true;
- }
+ if (_owner.ItemsSourceView.HasKeyIndexMapping)
+ {
+ _isDataSourceStableResetPending = true;
+ }
- // Walk through all the elements and make sure they are cleared, they will go into
- // the stable id reset pool.
- foreach (var element in _owner.Children)
- {
- var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
- if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
+ // Walk through all the elements and make sure they are cleared, they will go into
+ // the stable id reset pool.
+ foreach (var element in _owner.Children)
{
- _owner.ClearElementImpl(element);
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
+ {
+ _owner.ClearElementImpl(element);
+ }
}
}
@@ -441,6 +446,9 @@ namespace Avalonia.Controls
}
_resetPool.Clear();
+
+ // Flush the realized indices once the stable reset pool is cleared to start fresh.
+ InvalidateRealizedIndicesHeldByLayout();
}
}
@@ -498,6 +506,10 @@ namespace Avalonia.Controls
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
+
+ // Update realized indices
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
}
}
@@ -519,6 +531,10 @@ namespace Avalonia.Controls
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
+
+ // Update realized indices
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break;
}
}
diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs
index dd4934f9e5..93699583e6 100644
--- a/src/Avalonia.Controls/SelectionModel.cs
+++ b/src/Avalonia.Controls/SelectionModel.cs
@@ -20,6 +20,7 @@ namespace Avalonia.Controls
private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
+ private IndexPath _oldAnchorIndex;
private IReadOnlyList? _selectedIndicesCached;
private IReadOnlyList
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DisableVirtualizationProperty =
+ AvaloniaProperty.Register(nameof(DisableVirtualization));
+
///
/// Defines the property.
///
@@ -36,6 +42,15 @@ namespace Avalonia.Layout
LayoutId = "StackLayout";
}
+ ///
+ /// Gets or sets a value indicating whether virtualization is disabled on the layout.
+ ///
+ public bool DisableVirtualization
+ {
+ get => GetValue(DisableVirtualizationProperty);
+ set => SetValue(DisableVirtualizationProperty, value);
+ }
+
///
/// Gets or sets the axis along which items are laid out.
///
@@ -262,6 +277,8 @@ namespace Avalonia.Layout
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
+ ((StackLayoutState)context.LayoutState).OnMeasureStart();
+
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
@@ -270,6 +287,7 @@ namespace Avalonia.Layout
Spacing,
int.MaxValue,
_orientation.ScrollOrientation,
+ DisableVirtualization,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
@@ -284,8 +302,6 @@ namespace Avalonia.Layout
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
- ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
-
return new Size(value.Width, value.Height);
}
diff --git a/src/Avalonia.Layout/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs
index 05ad9bca8e..e6164e02e6 100644
--- a/src/Avalonia.Layout/StackLayoutState.cs
+++ b/src/Avalonia.Layout/StackLayoutState.cs
@@ -56,6 +56,6 @@ namespace Avalonia.Layout
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
}
- internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
+ internal void OnMeasureStart() => MaxArrangeBounds = 0;
}
}
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
index ee9cff4a01..d565fae074 100644
--- a/src/Avalonia.Layout/UniformGridLayout.cs
+++ b/src/Avalonia.Layout/UniformGridLayout.cs
@@ -433,6 +433,7 @@ namespace Avalonia.Layout
LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation,
+ false,
LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs
index 5ff16c574d..5c769ad48c 100644
--- a/src/Avalonia.Remote.Protocol/DesignMessages.cs
+++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs
@@ -1,4 +1,7 @@
using System;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Xml;
namespace Avalonia.Remote.Protocol.Designer
{
@@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer
public class ExceptionDetails
{
+ public ExceptionDetails()
+ {
+ }
+
+ public ExceptionDetails(Exception e)
+ {
+ if (e is TargetInvocationException)
+ {
+ e = e.InnerException;
+ }
+
+ ExceptionType = e.GetType().Name;
+ Message = e.Message;
+
+ if (e is XmlException xml)
+ {
+ LineNumber = xml.LineNumber;
+ LinePosition = xml.LinePosition;
+ }
+ }
+
public string ExceptionType { get; set; }
public string Message { get; set; }
public int? LineNumber { get; set; }
diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs
index fd187f6409..9e4d6aacb0 100644
--- a/src/Avalonia.Visuals/Media/BoxShadows.cs
+++ b/src/Avalonia.Visuals/Media/BoxShadows.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Media
{
_first = shadow;
_list = null;
- Count = 1;
+ Count = _first.IsEmpty ? 0 : 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
@@ -105,8 +105,6 @@ namespace Avalonia.Media
return false;
}
}
-
- public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other)
{
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index 4df26c470d..1a45b8342a 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -141,13 +141,13 @@ namespace Avalonia.Media
/// The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
///
- /// Box shadow effect parameters
+ /// Box shadow effect parameters
///
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
///
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
- BoxShadow boxShadow = default)
+ BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
{
@@ -164,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2);
}
- PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
+ PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
}
///
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index b8658a7a26..6ad71ac111 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -364,7 +364,7 @@ namespace Avalonia.Rendering.SceneGraph
public int DrawOperationIndex { get; }
}
- private void Add(IDrawOperation node)
+ private void Add(T node) where T : class, IDrawOperation
{
using (var refCounted = RefCountable.Create(node))
{
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 28e87dd671..9a81d19bb9 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -1,7 +1,10 @@
using System;
using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Platform;
using Avalonia.UnitTests;
+using Castle.DynamicProxy.Generators;
using Moq;
using Xunit;
@@ -168,6 +171,90 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Context_Menu_In_Resources_Can_Be_Shared()
+ {
+ using (Application())
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+
+
+";
+
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var target1 = window.Find("target1");
+ var target2 = window.Find("target2");
+ var mouse = new MouseTestHelper();
+
+ Assert.NotNull(target1.ContextMenu);
+ Assert.NotNull(target2.ContextMenu);
+ Assert.Same(target1.ContextMenu, target2.ContextMenu);
+
+ window.Show();
+
+ var menu = target1.ContextMenu;
+ mouse.Click(target1, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ mouse.Click(target2, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ }
+ }
+
+ [Fact]
+ public void Context_Menu_Can_Be_Set_In_Style()
+ {
+ using (Application())
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+";
+
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var target1 = window.Find("target1");
+ var target2 = window.Find("target2");
+ var mouse = new MouseTestHelper();
+
+ Assert.NotNull(target1.ContextMenu);
+ Assert.NotNull(target2.ContextMenu);
+ Assert.Same(target1.ContextMenu, target2.ContextMenu);
+
+ window.Show();
+
+ var menu = target1.ContextMenu;
+ mouse.Click(target1, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ mouse.Click(target2, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ }
+ }
+
[Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index d43385142d..a7679ba388 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
@@ -367,6 +367,46 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control()
+ {
+ // Issue #3934
+ var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = items,
+ ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }),
+ SelectionMode = SelectionMode.AlwaysSelected,
+ VirtualizationMode = ItemVirtualizationMode.None,
+ };
+
+ Prepare(target);
+
+ // First an item that is not index 0 must be selected.
+ _mouse.Click(target.Presenter.Panel.Children[1]);
+ Assert.Equal(new IndexPath(1), target.Selection.AnchorIndex);
+
+ // We're going to be clicking on item 9.
+ var item = (ListBoxItem)target.Presenter.Panel.Children[9];
+ var raised = 0;
+
+ // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled
+ // by the ScrollContentPresenter as the item is already visible, so we don't need
+ // handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled
+ // into view due to SelectionMode.AlwaysSelected.
+ target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) =>
+ {
+ Assert.Same(item, e.TargetObject);
+ ++raised;
+ });
+
+ // Click item 9.
+ _mouse.Click(item);
+
+ Assert.Equal(1, raised);
+ }
+
private FuncControlTemplate ListBoxTemplate()
{
return new FuncControlTemplate((parent, scope) =>
diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
index 246ff723a1..ebf9c40012 100644
--- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
@@ -1458,6 +1458,60 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised);
}
+ [Fact]
+ public void Batch_Update_Does_Not_Raise_PropertyChanged_Until_Operation_Finished()
+ {
+ var data = new[] { "foo", "bar", "baz", "qux" };
+ var target = new SelectionModel { Source = data };
+ var raised = 0;
+
+ target.SelectedIndex = new IndexPath(1);
+
+ Assert.Equal(new IndexPath(1), target.AnchorIndex);
+
+ target.PropertyChanged += (s, e) => ++raised;
+
+ using (target.Update())
+ {
+ target.ClearSelection();
+
+ Assert.Equal(0, raised);
+
+ target.AnchorIndex = new IndexPath(2);
+
+ Assert.Equal(0, raised);
+
+ target.SelectedIndex = new IndexPath(3);
+
+ Assert.Equal(0, raised);
+ }
+
+ Assert.Equal(new IndexPath(3), target.AnchorIndex);
+ Assert.Equal(5, raised);
+ }
+
+ [Fact]
+ public void Batch_Update_Does_Not_Raise_PropertyChanged_If_Nothing_Changed()
+ {
+ var data = new[] { "foo", "bar", "baz", "qux" };
+ var target = new SelectionModel { Source = data };
+ var raised = 0;
+
+ target.SelectedIndex = new IndexPath(1);
+
+ Assert.Equal(new IndexPath(1), target.AnchorIndex);
+
+ target.PropertyChanged += (s, e) => ++raised;
+
+ using (target.Update())
+ {
+ target.ClearSelection();
+ target.SelectedIndex = new IndexPath(1);
+ }
+
+ Assert.Equal(0, raised);
+ }
+
[Fact]
public void AutoSelect_Selects_When_Enabled()
{