Browse Source

Merge branch 'master' into feature/decode-bitmap-at-sepcified-size

pull/3890/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
5b5414eb23
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Documentation/build.md
  2. 2
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  3. 39
      src/Avalonia.Controls/ContextMenu.cs
  4. 28
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  5. 36
      src/Avalonia.Controls/Repeater/ViewManager.cs
  6. 35
      src/Avalonia.Controls/SelectionModel.cs
  7. 10
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  8. 10
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  9. 20
      src/Avalonia.Layout/StackLayout.cs
  10. 2
      src/Avalonia.Layout/StackLayoutState.cs
  11. 1
      src/Avalonia.Layout/UniformGridLayout.cs
  12. 24
      src/Avalonia.Remote.Protocol/DesignMessages.cs
  13. 4
      src/Avalonia.Visuals/Media/BoxShadows.cs
  14. 6
      src/Avalonia.Visuals/Media/DrawingContext.cs
  15. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  16. 87
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  17. 40
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  18. 54
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS: 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): On Debian based Linux (Debian, Ubuntu, Mint, etc):

2
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@ -17,7 +17,7 @@ namespace RenderDemo.Controls
public LineBoundsDemoControl() public LineBoundsDemoControl()
{ {
var timer = new DispatcherTimer(); 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.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start(); timer.Start();
} }

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// A control context menu. /// A control context menu.
/// </summary> /// </summary>
public class ContextMenu : MenuBase public class ContextMenu : MenuBase, ISetterValue
{ {
private static readonly ITemplate<IPanel> DefaultPanel = private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical }); new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup; private Popup _popup;
private Control _attachedControl; private List<Control> _attachedControls;
private IInputElement _previousFocus; private IInputElement _previousFocus;
/// <summary> /// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu) if (e.OldValue is ContextMenu oldMenu)
{ {
control.PointerReleased -= ControlPointerReleased; control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null; oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null); ((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
} }
if (e.NewValue is ContextMenu newMenu) if (e.NewValue is ContextMenu newMenu)
{ {
newMenu._attachedControl = control; newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased; control.PointerReleased += ControlPointerReleased;
} }
} }
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
public void Open(Control 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)); 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( throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.", "Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control)); nameof(control));
} }
control ??= _attachedControls[0];
if (IsOpen) if (IsOpen)
{ {
return; return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
} }
((ISetLogicalParent)_popup).SetParent(control); if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this; _popup.Child = this;
_popup.IsOpen = true; _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 <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new MenuItemContainerGenerator(this); return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1; SelectedIndex = -1;
IsOpen = false; IsOpen = false;
if (_attachedControl is null) if (_attachedControls is null || _attachedControls.Count == 0)
{ {
((ISetLogicalParent)_popup).SetParent(null); ((ISetLogicalParent)_popup).SetParent(null);
} }

28
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -379,14 +380,19 @@ namespace Avalonia.Controls
{ {
if (property == ItemsProperty) if (property == ItemsProperty)
{ {
var oldEnumerable = oldValue.GetValueOrDefault<IEnumerable>();
var newEnumerable = newValue.GetValueOrDefault<IEnumerable>(); var newEnumerable = newValue.GetValueOrDefault<IEnumerable>();
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) else if (property == ItemTemplateProperty)
{ {
@ -431,8 +437,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element) private int GetElementIndexImpl(IControl element)
{ {
var virtInfo = TryGetVirtualizationInfo(element); // Verify that element is actually a child of this ItemsRepeater
return _viewManager.GetElementIndex(virtInfo); var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
} }
private IControl GetElementFromIndexImpl(int index) private IControl GetElementFromIndexImpl(int index)

36
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -388,19 +388,24 @@ namespace Avalonia.Controls
} }
case NotifyCollectionChangedAction.Reset: 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 // Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool. // the stable id reset pool.
foreach (var element in _owner.Children) foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{ {
_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(); _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); var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index); 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); _pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement; element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break; break;
} }
} }

35
src/Avalonia.Controls/SelectionModel.cs

@ -20,6 +20,7 @@ namespace Avalonia.Controls
private bool _singleSelect; private bool _singleSelect;
private bool _autoSelect; private bool _autoSelect;
private int _operationCount; private int _operationCount;
private IndexPath _oldAnchorIndex;
private IReadOnlyList<IndexPath>? _selectedIndicesCached; private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached; private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs; private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
@ -142,6 +143,8 @@ namespace Avalonia.Controls
} }
set set
{ {
var oldValue = AnchorIndex;
if (value != null) if (value != null)
{ {
SelectionTreeHelper.TraverseIndexPath( SelectionTreeHelper.TraverseIndexPath(
@ -155,7 +158,10 @@ namespace Avalonia.Controls
_rootNode.AnchorIndex = -1; _rootNode.AnchorIndex = -1;
} }
RaisePropertyChanged("AnchorIndex"); if (_operationCount == 0 && oldValue != AnchorIndex)
{
RaisePropertyChanged("AnchorIndex");
}
} }
} }
@ -633,19 +639,18 @@ namespace Avalonia.Controls
_selectedIndicesCached = null; _selectedIndicesCached = null;
_selectedItemsCached = null; _selectedItemsCached = null;
// Raise SelectionChanged event
if (e != null) if (e != null)
{ {
SelectionChanged?.Invoke(this, e); SelectionChanged?.Invoke(this, e);
}
RaisePropertyChanged(nameof(SelectedIndex)); RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices)); RaisePropertyChanged(nameof(SelectedIndices));
if (_rootNode.Source != null) if (_rootNode.Source != null)
{ {
RaisePropertyChanged(nameof(SelectedItem)); RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems)); RaisePropertyChanged(nameof(SelectedItems));
}
} }
} }
@ -785,6 +790,7 @@ namespace Avalonia.Controls
{ {
if (_operationCount++ == 0) if (_operationCount++ == 0)
{ {
_oldAnchorIndex = AnchorIndex;
_rootNode.BeginOperation(); _rootNode.BeginOperation();
} }
} }
@ -808,13 +814,16 @@ namespace Avalonia.Controls
var changeSet = new SelectionModelChangeSet(changes); var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs(); e = changeSet.CreateEventArgs();
} }
}
OnSelectionChanged(e); OnSelectionChanged(e);
if (_oldAnchorIndex != AnchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
if (_operationCount == 0)
{
_rootNode.Cleanup(); _rootNode.Cleanup();
_oldAnchorIndex = default;
} }
} }

10
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -234,18 +234,10 @@ namespace Avalonia.DesignerSupport.Remote
} }
catch (Exception e) catch (Exception e)
{ {
var xmlException = e as XmlException;
s_transport.Send(new UpdateXamlResultMessage s_transport.Send(new UpdateXamlResultMessage
{ {
Error = e.ToString(), Error = e.ToString(),
Exception = new ExceptionDetails Exception = new ExceptionDetails(e),
{
ExceptionType = e.GetType().FullName,
Message = e.Message.ToString(),
LineNumber = xmlException?.LineNumber,
LinePosition = xmlException?.LinePosition,
}
}); });
} }
} }

10
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@ -74,6 +74,7 @@ namespace Avalonia.Layout
double lineSpacing, double lineSpacing,
int maxItemsPerLine, int maxItemsPerLine,
ScrollOrientation orientation, ScrollOrientation orientation,
bool disableVirtualization,
string layoutId) string layoutId)
{ {
_orientation.ScrollOrientation = orientation; _orientation.ScrollOrientation = orientation;
@ -95,14 +96,14 @@ namespace Avalonia.Layout
_elementManager.OnBeginMeasure(orientation); _elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired()) if (isWrapping && IsReflowRequired())
{ {
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0); _orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
} }
RaiseLineArranged(); RaiseLineArranged();
@ -273,6 +274,7 @@ namespace Avalonia.Layout
double minItemSpacing, double minItemSpacing,
double lineSpacing, double lineSpacing,
int maxItemsPerLine, int maxItemsPerLine,
bool disableVirtualization,
string layoutId) string layoutId)
{ {
if (anchorIndex != -1) if (anchorIndex != -1)
@ -288,7 +290,7 @@ namespace Avalonia.Layout
bool lineNeedsReposition = false; bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) && while (_elementManager.IsIndexValidInData(currentIndex) &&
ShouldContinueFillingUpSpace(previousIndex, direction)) (disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
{ {
// Ensure layout element. // Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);

20
src/Avalonia.Layout/StackLayout.cs

@ -14,6 +14,12 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{ {
/// <summary>
/// Defines the <see cref="DisableVirtualization"/> property.
/// </summary>
public static readonly StyledProperty<bool> DisableVirtualizationProperty =
AvaloniaProperty.Register<StackLayout, bool>(nameof(DisableVirtualization));
/// <summary> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
/// </summary> /// </summary>
@ -36,6 +42,15 @@ namespace Avalonia.Layout
LayoutId = "StackLayout"; LayoutId = "StackLayout";
} }
/// <summary>
/// Gets or sets a value indicating whether virtualization is disabled on the layout.
/// </summary>
public bool DisableVirtualization
{
get => GetValue(DisableVirtualizationProperty);
set => SetValue(DisableVirtualizationProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the axis along which items are laid out. /// Gets or sets the axis along which items are laid out.
/// </summary> /// </summary>
@ -262,6 +277,8 @@ namespace Avalonia.Layout
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{ {
((StackLayoutState)context.LayoutState).OnMeasureStart();
var desiredSize = GetFlowAlgorithm(context).Measure( var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize, availableSize,
context, context,
@ -270,6 +287,7 @@ namespace Avalonia.Layout
Spacing, Spacing,
int.MaxValue, int.MaxValue,
_orientation.ScrollOrientation, _orientation.ScrollOrientation,
DisableVirtualization,
LayoutId); LayoutId);
return new Size(desiredSize.Width, desiredSize.Height); return new Size(desiredSize.Width, desiredSize.Height);
@ -284,8 +302,6 @@ namespace Avalonia.Layout
FlowLayoutAlgorithm.LineAlignment.Start, FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId); LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
return new Size(value.Width, value.Height); return new Size(value.Width, value.Height);
} }

2
src/Avalonia.Layout/StackLayoutState.cs

@ -56,6 +56,6 @@ namespace Avalonia.Layout
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize); MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
} }
internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0; internal void OnMeasureStart() => MaxArrangeBounds = 0;
} }
} }

1
src/Avalonia.Layout/UniformGridLayout.cs

@ -433,6 +433,7 @@ namespace Avalonia.Layout
LineSpacing, LineSpacing,
_maximumRowsOrColumns, _maximumRowsOrColumns,
_orientation.ScrollOrientation, _orientation.ScrollOrientation,
false,
LayoutId); LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership, // If after Measure the first item is in the realization rect, then we revoke grid state's ownership,

24
src/Avalonia.Remote.Protocol/DesignMessages.cs

@ -1,4 +1,7 @@
using System; using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Xml;
namespace Avalonia.Remote.Protocol.Designer namespace Avalonia.Remote.Protocol.Designer
{ {
@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer
public class ExceptionDetails 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 ExceptionType { get; set; }
public string Message { get; set; } public string Message { get; set; }
public int? LineNumber { get; set; } public int? LineNumber { get; set; }

4
src/Avalonia.Visuals/Media/BoxShadows.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media
{ {
_first = shadow; _first = shadow;
_list = null; _list = null;
Count = 1; Count = _first.IsEmpty ? 0 : 1;
} }
public BoxShadows(BoxShadow first, BoxShadow[] rest) public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -105,8 +105,6 @@ namespace Avalonia.Media
return false; return false;
} }
} }
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other) public bool Equals(BoxShadows other)
{ {

6
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -141,13 +141,13 @@ namespace Avalonia.Media
/// <param name="radiusY">The radius in the Y dimension of the rounded corners. /// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2 /// This value will be clamped to the range of 0 to Height/2
/// </param> /// </param>
/// <param name="boxShadow">Box shadow effect parameters</param> /// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks> /// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// 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. /// 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.
/// </remarks> /// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, 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)) if (brush == null && !PenIsVisible(pen))
{ {
@ -164,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2); 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);
} }
/// <summary> /// <summary>

2
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -364,7 +364,7 @@ namespace Avalonia.Rendering.SceneGraph
public int DrawOperationIndex { get; } public int DrawOperationIndex { get; }
} }
private void Add(IDrawOperation node) private void Add<T>(T node) where T : class, IDrawOperation
{ {
using (var refCounted = RefCountable.Create(node)) using (var refCounted = RefCountable.Create(node))
{ {

87
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -1,7 +1,10 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Castle.DynamicProxy.Generators;
using Moq; using Moq;
using Xunit; using Xunit;
@ -168,6 +171,90 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Context_Menu_In_Resources_Can_Be_Shared()
{
using (Application())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<ContextMenu x:Key='contextMenu'>
<MenuItem>Foo</MenuItem>
</ContextMenu>
</Window.Resources>
<StackPanel>
<TextBlock Name='target1' ContextMenu='{StaticResource contextMenu}'/>
<TextBlock Name='target2' ContextMenu='{StaticResource contextMenu}'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target1 = window.Find<TextBlock>("target1");
var target2 = window.Find<TextBlock>("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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='ContextMenu'>
<ContextMenu>
<MenuItem>Foo</MenuItem>
</ContextMenu>
</Setter>
</Style>
</Window.Styles>
<StackPanel>
<TextBlock Name='target1'/>
<TextBlock Name='target2'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target1 = window.Find<TextBlock>("target1");
var target2 = window.Find<TextBlock>("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()")] [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() public void Cancelling_Closing_Leaves_ContextMenuOpen()
{ {

40
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<string>((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() private FuncControlTemplate ListBoxTemplate()
{ {
return new FuncControlTemplate<ListBox>((parent, scope) => return new FuncControlTemplate<ListBox>((parent, scope) =>

54
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

@ -1458,6 +1458,60 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised); 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] [Fact]
public void AutoSelect_Selects_When_Enabled() public void AutoSelect_Selects_When_Enabled()
{ {

Loading…
Cancel
Save