Browse Source

Merge remote-tracking branch 'upstream/master' into feature/invariantFamilyNames

pull/10620/head
Benedikt Stebner 3 years ago
parent
commit
bfb6315d6b
  1. 1
      .gitignore
  2. 2
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  3. 2
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  4. 2
      native/Avalonia.Native/src/OSX/WindowImpl.h
  5. 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  6. 13
      src/Avalonia.Base/AvaloniaProperty.cs
  7. 2
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 4
      src/Avalonia.Base/Media/DrawingGroup.cs
  9. 2
      src/Avalonia.Base/Media/FontManager.cs
  10. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  11. 1
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  12. 2
      src/Avalonia.Base/Rendering/DisplayDirtyRect.cs
  13. 1
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  14. 1
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  15. 1
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  16. 1
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  17. 2
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  18. 23
      src/Avalonia.Controls/ItemsSourceView.cs
  19. 2
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  20. 2
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  21. 17
      src/Avalonia.Controls/Selection/SelectedItems.cs
  22. 91
      src/Avalonia.Controls/Selection/SelectionModel.cs
  23. 8
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  24. 311
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  25. 2
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  26. 2
      src/Avalonia.Themes.Simple/Accents/Base.xaml
  27. 2
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  28. 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  29. 11
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

1
.gitignore

@ -102,6 +102,7 @@ csx
AppPackages/
# NCrunch
.NCrunch_*/
_NCrunch_*/
*.ncrunchsolution.user
nCrunchTemp_*

2
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -394,7 +394,7 @@
- (BOOL)windowShouldZoom:(NSWindow *_Nonnull)window toFrame:(NSRect)newFrame
{
return true;
return _parent->CanZoom();
}
-(void)windowDidResignKey:(NSNotification *)notification

2
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -107,6 +107,8 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override;
virtual bool CanZoom() { return false; }
protected:
virtual NSWindowStyleMask CalculateStyleMask() = 0;
virtual void UpdateStyle();

2
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -97,6 +97,8 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow ();
bool CanZoom() override { return _isEnabled && _canResize; }
protected:
virtual NSWindowStyleMask CalculateStyleMask() override;
void UpdateStyle () override;

2
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -622,5 +622,5 @@ void WindowImpl::UpdateStyle() {
[miniaturizeButton setHidden:!hasTrafficLights];
[miniaturizeButton setEnabled:_isEnabled];
[zoomButton setHidden:!hasTrafficLights];
[zoomButton setEnabled:_isEnabled && _canResize];
[zoomButton setEnabled:CanZoom()];
}

13
src/Avalonia.Base/AvaloniaProperty.cs

@ -257,7 +257,18 @@ namespace Avalonia
return result;
}
/// <inheritdoc cref="Register{TOwner, TValue}" />
/// <summary>
/// Registers an attached <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="TOwner">The type of the class that is registering the property.</typeparam>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <param name="name">The name of the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="enableDataValidation">if is set to true enable data validation.</param>
/// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an
/// object; the bool argument will be true before and false afterwards. This callback is

2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -364,7 +364,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, Action{AvaloniaObject, bool})"/>
/// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, bool)"/>
/// method.
/// </remarks>
public void Register(Type type, AvaloniaProperty property)

4
src/Avalonia.Base/Media/DrawingGroup.cs

@ -117,10 +117,10 @@ namespace Avalonia.Media
// root DrawingGroup, and be the same value as the root _currentDrawingGroup.
//
// Either way, _rootDrawing always references the root drawing.
protected Drawing? _rootDrawing;
private Drawing? _rootDrawing;
// Current DrawingGroup that new children are added to
protected DrawingGroup? _currentDrawingGroup;
private DrawingGroup? _currentDrawingGroup;
// Previous values of _currentDrawingGroup
private Stack<DrawingGroup?>? _previousDrawingGroupStack;

2
src/Avalonia.Base/Media/FontManager.cs

@ -107,7 +107,7 @@ namespace Avalonia.Media
source = new Uri(key.BaseUri, source);
}
if (!_fontCollections.TryGetValue(source, out var fontCollection))
if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
/// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="textRuns"></param>
/// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
/// <param name="canReuseTextRunList">Whether <see cref="TextRun"/> can be reused to store the split runs.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>

1
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -128,6 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">where to apply the opacity.</param>
void PushOpacity(double opacity, Rect bounds);
/// <summary>

2
src/Avalonia.Base/Rendering/DisplayDirtyRect.cs

@ -3,7 +3,7 @@
namespace Avalonia.Rendering
{
/// <summary>
/// Holds the state for a dirty rect rendered when <see cref="IRenderer.DrawDirtyRects"/> is set.
/// Holds the state for a dirty rect rendered when <see cref="IRenderer.SceneInvalidated"/> is set.
/// </summary>
internal class DisplayDirtyRect
{

1
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public GeometryNode(Matrix transform,
IImmutableBrush? brush,
IPen? pen,

1
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The start point of the line.</param>
/// <param name="p2">The end point of the line.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public LineNode(
Matrix transform,
IPen pen,

1
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
/// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
: base(default, Matrix.Identity, mask)
{

1
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -20,7 +20,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="boxShadows">The box shadow parameters</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public RectangleNode(
Matrix transform,
IImmutableBrush? brush,

2
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -52,7 +52,7 @@ namespace Avalonia.Styling
return result;
}
protected TypeNameAndClassSelector(Selector? previous)
TypeNameAndClassSelector(Selector? previous)
{
_previous = previous;
}

23
src/Avalonia.Controls/ItemsSourceView.cs

@ -27,6 +27,7 @@ namespace Avalonia.Controls
private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
@ -70,7 +71,7 @@ namespace Avalonia.Controls
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks>
internal bool HasKeyIndexMapping => false;
@ -92,6 +93,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PreCollectionChanged
{
add
{
AddListenerIfNecessary();
_preCollectionChanged += value;
}
remove
{
_preCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
@ -229,6 +249,7 @@ namespace Avalonia.Controls
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)

2
src/Avalonia.Controls/Platform/IInsetsManager.cs

@ -36,7 +36,7 @@ namespace Avalonia.Controls.Platform
SafeAreaPadding = safeArePadding;
}
/// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
/// <inheritdoc cref="IInsetsManager.SafeAreaPadding"/>
public Thickness SafeAreaPadding { get; }
}

2
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -203,7 +203,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{

17
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Controls.Selection
{
internal class SelectedItems<T> : IReadOnlyList<T>
internal class SelectedItems<T> : IReadOnlyList<T?>
{
private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items;
@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection
_items = items;
}
[MaybeNull]
public T this[int index]
public T? this[int index]
{
#pragma warning disable CS8766
get
#pragma warning restore CS8766
{
if (index >= Count)
{
@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator()
public IEnumerator<T?> GetEnumerator()
{
if (_owner?.SingleSelect == true)
{
if (_owner.SelectedIndex >= 0)
{
#pragma warning disable CS8603
yield return _owner.SelectedItem;
#pragma warning restore CS8603
}
}
else
@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8603
yield return items is object ? items[i] : default;
#pragma warning restore CS8603
}
}
}
@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection
public class Untyped : IReadOnlyList<object?>
{
private readonly IReadOnlyList<T> _source;
public Untyped(IReadOnlyList<T> source) => _source = source;
private readonly IReadOnlyList<T?> _source;
public Untyped(IReadOnlyList<T?> source) => _source = source;
public object? this[int index] => _source[index];
public int Count => _source.Count;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

91
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
private IList? _initSelectedItems;
private bool _isSourceCollectionChanging;
public SelectionModel()
{
@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection
if (RangesEnabled && _selectedIndex >= 0)
{
CommitSelect(new IndexRange(_selectedIndex));
CommitSelect(_selectedIndex, _selectedIndex);
}
RaisePropertyChanged(nameof(SingleSelect));
@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection
{
get
{
if (ItemsView is object)
if (ItemsView is not null)
{
return GetItemAt(_selectedIndex);
}
@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection
}
set
{
if (ItemsView is object)
if (ItemsView is not null)
{
SelectedIndex = ItemsView.IndexOf(value!);
}
else
{
Clear();
#pragma warning disable CS8601
SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
SetInitSelectedItems(new T[] { value! });
}
}
}
public IReadOnlyList<T> SelectedItems
public IReadOnlyList<T?> SelectedItems
{
get
{
@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection
{
// If the collection is currently changing, commit the update when the
// collection change finishes.
if (!IsSourceCollectionChanging)
if (!_isSourceCollectionChanging)
{
CommitOperation(_operation);
}
@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection
{
if (base.Source != value)
{
if (_operation is object)
if (_operation is not null)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection
{
update.Operation.IsSourceUpdate = true;
if (_initSelectedItems is object && ItemsView is object)
if (_initSelectedItems is object && ItemsView is not null)
{
foreach (T i in _initSelectedItems)
{
@ -315,17 +314,23 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
}
private protected override void OnSourceReset()
protected override void OnSourceCollectionChangeStarted()
{
base.OnSourceCollectionChangeStarted();
_isSourceCollectionChanging = true;
}
protected override void OnSourceReset()
{
_selectedIndex = _anchorIndex = -1;
CommitDeselect(new IndexRange(0, int.MaxValue));
CommitDeselect(0, int.MaxValue);
if (SourceReset is object)
if (SourceReset is not null)
{
SourceReset.Invoke(this, EventArgs.Empty);
}
@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
protected override void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
// Note: We're *not* putting this in a using scope. A collection update is still in progress
// so the operation won't get committed by normal means: we have to commit it manually.
@ -347,7 +352,7 @@ namespace Avalonia.Controls.Selection
update.Operation.DeselectedItems = deselectedItems;
if (_selectedIndex == -1 && LostSelection is object)
if (_selectedIndex == -1 && LostSelection is not null)
{
LostSelection(this, EventArgs.Empty);
}
@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection
CommitOperation(update.Operation, raisePropertyChanged: false);
}
private protected override CollectionChangeState OnItemsAdded(int index, IList items)
protected override CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = SelectedIndex >= index;
@ -420,7 +425,7 @@ namespace Avalonia.Controls.Selection
};
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_operation?.UpdateCount > 0)
{
@ -451,6 +456,16 @@ namespace Avalonia.Controls.Selection
}
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection
return true;
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
protected override void OnSourceCollectionChangeFinished()
{
if (_operation is object)
_isSourceCollectionChanging = false;
if (_operation is not null)
{
CommitOperation(_operation);
}
@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection
{
index = Math.Max(index, -1);
if (ItemsView is object && index >= ItemsView.Count)
if (ItemsView is not null && index >= ItemsView.Count)
{
index = -1;
}
@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection
private IndexRange CoerceRange(int start, int end)
{
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
var max = ItemsView is not null ? ItemsView.Count - 1 : int.MaxValue;
if (start > max || (start < 0 && end < 0))
{
@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection
var oldSelectedIndex = _selectedIndex;
var indexesChanged = false;
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection)
{
operation.UpdateCount++;
LostSelection?.Invoke(this, EventArgs.Empty);
@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection
_selectedIndex = operation.SelectedIndex;
_anchorIndex = operation.AnchorIndex;
if (operation.SelectedRanges is object)
if (operation.SelectedRanges is not null)
{
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
foreach (var range in operation.SelectedRanges)
{
indexesChanged |= CommitSelect(range.Begin, range.End) > 0;
}
}
if (operation.DeselectedRanges is object)
if (operation.DeselectedRanges is not null)
{
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
foreach (var range in operation.DeselectedRanges)
{
indexesChanged |= CommitDeselect(range.Begin, range.End) > 0;
}
}
if (SelectionChanged is object || _untypedSelectionChanged is object)
if (SelectionChanged is not null || _untypedSelectionChanged is not null)
{
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection
// CollectionChanged event. LostFocus may have caused another item to have been
// selected, but it can't have caused a deselection (as it was called due to
// selection being lost) so we're ok to discard `deselected` here.
var deselectedItems = operation.DeselectedItems ??
var deselectedItems = (IReadOnlyList<T?>?)operation.DeselectedItems ??
SelectedItems<T>.Create(deselected, deselectedSource);
var e = new SelectionModelSelectionChangedEventArgs<T>(
SelectedIndexes<T>.Create(deselected),
SelectedIndexes<T>.Create(selected),
deselectedItems,
SelectedItems<T>.Create(selected, ItemsView));
SelectedItems<T>.Create(selected, Source is not null ? ItemsView : null));
SelectionChanged?.Invoke(this, e);
_untypedSelectionChanged?.Invoke(this, e);
}

8
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null)
IReadOnlyList<T?>? deselectedItems = null,
IReadOnlyList<T?>? selectedItems = null)
{
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>();
@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public new IReadOnlyList<T> DeselectedItems { get; }
public new IReadOnlyList<T?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public new IReadOnlyList<T> SelectedItems { get; }
public new IReadOnlyList<T?> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{

311
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -2,37 +2,62 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Selection
{
public abstract class SelectionNodeBase<T> : ICollectionChangedListener
/// <summary>
/// Base class for selection models.
/// </summary>
/// <typeparam name="T">The type of the element being selected.</typeparam>
public abstract class SelectionNodeBase<T>
{
private IEnumerable? _source;
private bool _rangesEnabled;
private List<IndexRange>? _ranges;
private int _collectionChanging;
/// <summary>
/// Gets or sets the source collection.
/// </summary>
protected IEnumerable? Source
{
get => _source;
set
{
void OnPreChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeStarted();
void OnChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChanged(e);
void OnPostChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeFinished();
if (_source != value)
{
if (ItemsView?.Inner is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
if (ItemsView is not null)
{
ItemsView.PreCollectionChanged -= OnPreChanged;
ItemsView.CollectionChanged -= OnChanged;
ItemsView.PostCollectionChanged -= OnPostChanged;
}
_source = value;
ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
ItemsView = value is not null ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView is not null)
{
ItemsView.PreCollectionChanged += OnPreChanged;
ItemsView.CollectionChanged += OnChanged;
ItemsView.PostCollectionChanged += OnPostChanged;
}
}
}
}
protected bool IsSourceCollectionChanging => _collectionChanging > 0;
/// <summary>
/// Gets an <see cref="ItemsSourceView{T}"/> of the <see cref="Source"/>.
/// </summary>
protected internal ItemsSourceView<T>? ItemsView { get; set; }
/// <summary>
/// Gets or sets a value indicating whether range selection is currently enabled for
/// the selection node.
/// </summary>
protected bool RangesEnabled
{
get => _rangesEnabled;
@ -50,8 +75,6 @@ namespace Avalonia.Controls.Selection
}
}
internal ItemsSourceView<T>? ItemsView { get; set; }
internal IReadOnlyList<IndexRange> Ranges
{
get
@ -65,81 +88,170 @@ namespace Avalonia.Controls.Selection
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Called when the source collection starts changing.
/// </summary>
protected virtual void OnSourceCollectionChangeStarted()
{
++_collectionChanging;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Called when the <see cref="Source"/> collection changes.
/// </summary>
/// <param name="e">The details of the collection change.</param>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> calls
/// <see cref="OnItemsAdded(int, IList)"/> and <see cref="OnItemsRemoved(int, IList)"/>
/// in order to calculate how the collection change affects the currently selected items.
/// It then calls <see cref="OnIndexesChanged(int, int)"/> and
/// <see cref="OnSelectionRemoved(int, int, IReadOnlyList{T})"/> if necessary, according
/// to the <see cref="CollectionChangeState"/> returned by those methods.
///
/// Override this method and <see cref="OnSourceCollectionChangeFinished"/> to provide
/// custom handling of source collection changes.
/// </remarks>
protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
OnSourceCollectionChanged(e);
}
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (--_collectionChanging == 0)
if (!IsValidCollectionChange(e))
{
OnSourceCollectionChangeFinished();
return;
}
}
protected abstract void OnSourceCollectionChangeFinished();
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
private protected abstract void OnSourceReset();
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems);
private protected int CommitSelect(IndexRange range)
{
if (RangesEnabled)
switch (e.Action)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, range);
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
return 0;
if (shiftDelta != 0)
OnIndexesChanged(shiftIndex, shiftDelta);
if (removed is not null)
OnSelectionRemoved(shiftIndex, -shiftDelta, removed);
}
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
/// <summary>
/// Called when the source collection has finished changing, and all CollectionChanged
/// handlers have run.
/// </summary>
/// <remarks>
/// Override this method to respond to the end of a collection change instead of acting at
/// the end of <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// in order to ensure that all UI subscribers to the source collection change event have
/// had chance to run.
/// </remarks>
protected virtual void OnSourceCollectionChangeFinished()
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, ranges);
}
}
return 0;
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the indexes changed by the collection changing.
/// </summary>
/// <param name="shiftIndex">The first index that was shifted.</param>
/// <param name="shiftDelta">
/// If positive, the number of items inserted, or if negative the number of items removed.
/// </param>
protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
}
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// on collection reset.
/// </summary>
protected abstract void OnSourceReset();
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the items removed by a collection change.
/// </summary>
protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
}
private protected int CommitDeselect(IndexRange range)
/// <summary>
/// If <see cref="RangesEnabled"/>, adds the specified range to the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to select.</param>
/// <param name="end">The inclusive index of the end of the range to select.</param>
/// <returns>The number of items selected.</returns>
protected int CommitSelect(int begin, int end)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, range);
return IndexRange.Add(_ranges, new IndexRange(begin, end));
}
return 0;
}
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
/// <summary>
/// If <see cref="RangesEnabled"/>, removes the specified range from the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to deselect.</param>
/// <param name="end">The inclusive index of the end of the range to deselect.</param>
/// <returns>The number of items selected.</returns>
protected int CommitDeselect(int begin, int end)
{
if (RangesEnabled && _ranges is object)
if (RangesEnabled)
{
return IndexRange.Remove(_ranges, ranges);
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, new IndexRange(begin, end));
}
return 0;
}
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are added to the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are added.
/// </remarks>
protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = false;
if (_ranges is object)
if (_ranges is not null)
{
List<IndexRange>? toAdd = null;
@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
var begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
@ -167,7 +279,7 @@ namespace Avalonia.Controls.Selection
}
}
if (toAdd is object)
if (toAdd is not null)
{
foreach (var range in toAdd)
{
@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection
};
}
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are removed from the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are removed.
/// </remarks>
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
{
var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1);
bool shifted = false;
var shifted = false;
List<T>? removed = null;
if (_ranges is object)
if (_ranges is not null)
{
var deselected = new List<IndexRange>();
@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection
};
}
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
if (shiftDelta != 0)
{
OnIndexesChanged(shiftIndex, shiftDelta);
}
if (removed is object)
{
OnSelectionChanged(removed);
}
}
private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
// If the selection is modified in a CollectionChanged handler before the selection
@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection
return true;
}
private protected struct CollectionChangeState
/// <summary>
/// Details the results of a collection change on the current selection;
/// </summary>
protected class CollectionChangeState
{
public int ShiftIndex;
public int ShiftDelta;
public List<T>? RemovedItems;
/// <summary>
/// Gets or sets the first index that was shifted as a result of the collection
/// changing.
/// </summary>
public int ShiftIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating how the indexes after <see cref="ShiftIndex"/>
/// were shifted.
/// </summary>
public int ShiftDelta { get; set; }
/// <summary>
/// Gets or sets the items removed by the collection change, if any.
/// </summary>
public List<T>? RemovedItems { get; set; }
}
}
}

2
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -432,7 +432,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Called when the <see cref="FlyoutBase.Placement"/> property changes.
/// Called when the <see cref="PopupFlyoutBase.Placement"/> property changes.
/// </summary>
private void Flyout_PlacementPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{

2
src/Avalonia.Themes.Simple/Accents/Base.xaml

@ -76,7 +76,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts://Inter#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
<Color x:Key="ThemeAccentColor">#CC119EDA</Color>
<Color x:Key="ThemeAccentColor2">#99119EDA</Color>
<Color x:Key="ThemeAccentColor3">#66119EDA</Color>

2
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -16,7 +16,7 @@ public class BrowserPlatformOptions
public static class BrowserAppBuilder
{
/// <summary>
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see cref="mainDivId"/> parameter.
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see paramref="mainDivId"/> parameter.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>

1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -29,7 +29,6 @@ namespace Avalonia.Direct2D1.Media
/// <summary>
/// Initializes a new instance of the <see cref="DrawingContextImpl"/> class.
/// </summary>
/// <param name="visualBrushRenderer">The visual brush renderer.</param>
/// <param name="renderTarget">The render target to draw to.</param>
/// <param name="layerFactory">
/// An object to use to create layers. May be null, in which case a

11
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@ -76,5 +76,16 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Throws<InvalidOperationException>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
}
}
[Fact]
public void Should_Return_False_For_Unregistered_FontCollection_Uri()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var result = FontManager.Current.TryGetGlyphTypeface(new Typeface("fonts:invalid#Something"), out _);
Assert.False(result);
}
}
}
}

Loading…
Cancel
Save