committed by
GitHub
260 changed files with 9915 additions and 2113 deletions
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.0" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,25 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.ItemsRepeaterPage"> |
|||
<DockPanel> |
|||
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16"> |
|||
<TextBlock Classes="h1">ItemsRepeater</TextBlock> |
|||
<TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock> |
|||
</StackPanel> |
|||
<StackPanel DockPanel.Dock="Right" Margin="8 0"> |
|||
<ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged"> |
|||
<ComboBoxItem>Stack - Vertical</ComboBoxItem> |
|||
<ComboBoxItem>Stack - Horizontal</ComboBoxItem> |
|||
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem> |
|||
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem> |
|||
</ComboBox> |
|||
</StackPanel> |
|||
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16"> |
|||
<ScrollViewer Name="scroller" |
|||
HorizontalScrollBarVisibility="Auto" |
|||
VerticalScrollBarVisibility="Auto"> |
|||
<ItemsRepeater Name="repeater" Items="{Binding}"/> |
|||
</ScrollViewer> |
|||
</Border> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,71 @@ |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class ItemsRepeaterPage : UserControl |
|||
{ |
|||
private ItemsRepeater _repeater; |
|||
private ScrollViewer _scroller; |
|||
|
|||
public ItemsRepeaterPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
_repeater = this.FindControl<ItemsRepeater>("repeater"); |
|||
_scroller = this.FindControl<ScrollViewer>("scroller"); |
|||
DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
private void LayoutChanged(object sender, SelectionChangedEventArgs e) |
|||
{ |
|||
if (_repeater == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var comboBox = (ComboBox)sender; |
|||
|
|||
switch (comboBox.SelectedIndex) |
|||
{ |
|||
case 0: |
|||
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_repeater.Layout = new StackLayout { Orientation = Orientation.Vertical }; |
|||
break; |
|||
case 1: |
|||
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal }; |
|||
break; |
|||
case 2: |
|||
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; |
|||
_repeater.Layout = new UniformGridLayout |
|||
{ |
|||
Orientation = Orientation.Vertical, |
|||
MinItemWidth = 200, |
|||
MinItemHeight = 200, |
|||
}; |
|||
break; |
|||
case 3: |
|||
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; |
|||
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
_repeater.Layout = new UniformGridLayout |
|||
{ |
|||
Orientation = Orientation.Horizontal, |
|||
MinItemWidth = 200, |
|||
MinItemHeight = 200, |
|||
}; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<UserControl xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.TabStripPage" |
|||
xmlns="https://github.com/avaloniaui"> |
|||
<StackPanel Orientation="Vertical" Spacing="4"> |
|||
<TextBlock Classes="h1">TabStrip</TextBlock> |
|||
<TextBlock Classes="h2">A control which displays a selectable strip of tabs</TextBlock> |
|||
|
|||
<Separator Margin="0 16"/> |
|||
|
|||
<TextBlock Classes="h1">Defined in XAML</TextBlock> |
|||
<TabStrip> |
|||
<TabStripItem>Item 1</TabStripItem> |
|||
<TabStripItem>Item 2</TabStripItem> |
|||
<TabStripItem IsEnabled="False">Disabled</TabStripItem> |
|||
</TabStrip> |
|||
|
|||
<Separator Margin="0 16"/> |
|||
|
|||
<TextBlock Classes="h1">Dynamically generated</TextBlock> |
|||
<TabStrip Items="{Binding}"> |
|||
<TabStrip.Styles> |
|||
<Style Selector="TabStripItem"> |
|||
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> |
|||
</Style> |
|||
</TabStrip.Styles> |
|||
<TabStrip.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock Text="{Binding Header}"/> |
|||
</DataTemplate> |
|||
</TabStrip.ItemTemplate> |
|||
</TabStrip> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,45 @@ |
|||
using System; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class TabStripPage : UserControl |
|||
{ |
|||
public TabStripPage() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
DataContext = new[] |
|||
{ |
|||
new TabStripItemViewModel |
|||
{ |
|||
Header = "Item 1", |
|||
}, |
|||
new TabStripItemViewModel |
|||
{ |
|||
Header = "Item 2", |
|||
}, |
|||
new TabStripItemViewModel |
|||
{ |
|||
Header = "Disabled", |
|||
IsEnabled = false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
private class TabStripItemViewModel |
|||
{ |
|||
public string Header { get; set; } |
|||
public bool IsEnabled { get; set; } = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
internal sealed class AvaloniaPropertyValueStore<TValue> |
|||
{ |
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStore() |
|||
{ |
|||
// The last item in the list is always int.MaxValue
|
|||
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; |
|||
} |
|||
|
|||
private (int, bool) TryFindEntry(int propertyId) |
|||
{ |
|||
if (_entries.Length <= 12) |
|||
{ |
|||
// For small lists, we use an optimized linear search. Since the last item in the list
|
|||
// is always int.MaxValue, we can skip a conditional branch in each iteration.
|
|||
// By unrolling the loop, we can skip another unconditional branch in each iteration.
|
|||
|
|||
if (_entries[0].PropertyId >= propertyId) |
|||
return (0, _entries[0].PropertyId == propertyId); |
|||
if (_entries[1].PropertyId >= propertyId) |
|||
return (1, _entries[1].PropertyId == propertyId); |
|||
if (_entries[2].PropertyId >= propertyId) |
|||
return (2, _entries[2].PropertyId == propertyId); |
|||
if (_entries[3].PropertyId >= propertyId) |
|||
return (3, _entries[3].PropertyId == propertyId); |
|||
if (_entries[4].PropertyId >= propertyId) |
|||
return (4, _entries[4].PropertyId == propertyId); |
|||
if (_entries[5].PropertyId >= propertyId) |
|||
return (5, _entries[5].PropertyId == propertyId); |
|||
if (_entries[6].PropertyId >= propertyId) |
|||
return (6, _entries[6].PropertyId == propertyId); |
|||
if (_entries[7].PropertyId >= propertyId) |
|||
return (7, _entries[7].PropertyId == propertyId); |
|||
if (_entries[8].PropertyId >= propertyId) |
|||
return (8, _entries[8].PropertyId == propertyId); |
|||
if (_entries[9].PropertyId >= propertyId) |
|||
return (9, _entries[9].PropertyId == propertyId); |
|||
if (_entries[10].PropertyId >= propertyId) |
|||
return (10, _entries[10].PropertyId == propertyId); |
|||
} |
|||
else |
|||
{ |
|||
int low = 0; |
|||
int high = _entries.Length; |
|||
int id; |
|||
|
|||
while (high - low > 3) |
|||
{ |
|||
int pivot = (high + low) / 2; |
|||
id = _entries[pivot].PropertyId; |
|||
|
|||
if (propertyId == id) |
|||
return (pivot, true); |
|||
|
|||
if (propertyId <= id) |
|||
high = pivot; |
|||
else |
|||
low = pivot + 1; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
id = _entries[low].PropertyId; |
|||
|
|||
if (id == propertyId) |
|||
return (low, true); |
|||
|
|||
if (id > propertyId) |
|||
break; |
|||
|
|||
++low; |
|||
} |
|||
while (low < high); |
|||
} |
|||
|
|||
return (0, false); |
|||
} |
|||
|
|||
public bool TryGetValue(AvaloniaProperty property, out TValue value) |
|||
{ |
|||
(int index, bool found) = TryFindEntry(property.Id); |
|||
if (!found) |
|||
{ |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Entry[] entries = new Entry[_entries.Length + 1]; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (_entries[i].PropertyId > property.Id) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
Array.Copy(_entries, 0, entries, 0, i); |
|||
} |
|||
|
|||
entries[i] = new Entry { PropertyId = property.Id, Value = value }; |
|||
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
|
|||
public void SetValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
_entries[TryFindEntry(property.Id).Item1].Value = value; |
|||
} |
|||
|
|||
public Dictionary<AvaloniaProperty, TValue> ToDictionary() |
|||
{ |
|||
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1); |
|||
|
|||
for (int i = 0; i < _entries.Length - 1; ++i) |
|||
{ |
|||
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); |
|||
} |
|||
|
|||
return dict; |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
internal int PropertyId; |
|||
internal TValue Value; |
|||
} |
|||
} |
|||
} |
|||
@ -1,168 +1,122 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Callback invoked when deferred setter wants to set a value.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Value type.</typeparam>
|
|||
/// <param name="property">Property being set.</param>
|
|||
/// <param name="backing">Backing field reference.</param>
|
|||
/// <param name="value">New value.</param>
|
|||
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value); |
|||
|
|||
/// <summary>
|
|||
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
|
|||
/// Used to fix #855.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
|
|||
class DeferredSetter<TSetRecord> |
|||
internal sealed class DeferredSetter<TSetRecord> |
|||
{ |
|||
private struct NotifyDisposable : IDisposable |
|||
private readonly SingleOrQueue<TSetRecord> _pendingValues; |
|||
private bool _isNotifying; |
|||
|
|||
public DeferredSetter() |
|||
{ |
|||
private readonly SettingStatus status; |
|||
_pendingValues = new SingleOrQueue<TSetRecord>(); |
|||
} |
|||
|
|||
internal NotifyDisposable(SettingStatus status) |
|||
{ |
|||
this.status = status; |
|||
status.Notifying = true; |
|||
} |
|||
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value) |
|||
{ |
|||
var old = backing; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
status.Notifying = false; |
|||
} |
|||
backing = value; |
|||
|
|||
source.RaisePropertyChanged(property, old, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Information on current setting/notification status of a property.
|
|||
/// </summary>
|
|||
private class SettingStatus |
|||
public bool SetAndNotify( |
|||
AvaloniaObject source, |
|||
AvaloniaProperty<TSetRecord> property, |
|||
ref TSetRecord backing, |
|||
TSetRecord value) |
|||
{ |
|||
public bool Notifying { get; set; } |
|||
|
|||
private SingleOrQueue<TSetRecord> pendingValues; |
|||
|
|||
public SingleOrQueue<TSetRecord> PendingValues |
|||
if (!_isNotifying) |
|||
{ |
|||
get |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>()); |
|||
SetAndRaisePropertyChanged(source, property, ref backing, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords; |
|||
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords |
|||
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>()); |
|||
if (!_pendingValues.Empty) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
while (!_pendingValues.Empty) |
|||
{ |
|||
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private SettingStatus GetOrCreateStatus(AvaloniaProperty property) |
|||
{ |
|||
if (!SetRecords.TryGetValue(property, out var status)) |
|||
{ |
|||
status = new SettingStatus(); |
|||
SetRecords.Add(property, status); |
|||
return true; |
|||
} |
|||
|
|||
return status; |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Mark the property as currently notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to mark as notifying.</param>
|
|||
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
|
|||
private NotifyDisposable MarkNotifying(AvaloniaProperty property) |
|||
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value) |
|||
where TValue : TSetRecord |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); |
|||
|
|||
SettingStatus status = GetOrCreateStatus(property); |
|||
|
|||
return new NotifyDisposable(status); |
|||
} |
|||
if (!_isNotifying) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if the property is currently notifying listeners.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <returns>If the property is currently notifying listeners.</returns>
|
|||
private bool IsNotifying(AvaloniaProperty property) |
|||
=> SetRecords.TryGetValue(property, out var value) && value.Notifying; |
|||
if (!_pendingValues.Empty) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
while (!_pendingValues.Empty) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a pending assignment for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="value">The value to assign.</param>
|
|||
private void AddPendingSet(AvaloniaProperty property, TSetRecord value) |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(IsNotifying(property)); |
|||
return true; |
|||
} |
|||
|
|||
GetOrCreateStatus(property).PendingValues.Enqueue(value); |
|||
} |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
/// <summary>
|
|||
/// Checks if there are any pending assignments for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>If the property has any pending assignments.</returns>
|
|||
private bool HasPendingSet(AvaloniaProperty property) |
|||
{ |
|||
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the first pending assignment for the property.
|
|||
/// Disposable that marks the property as currently notifying.
|
|||
/// When disposed, marks the property as done notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>The first pending assignment for the property.</returns>
|
|||
private TSetRecord GetFirstPendingSet(AvaloniaProperty property) |
|||
private readonly struct NotifyDisposable : IDisposable |
|||
{ |
|||
return GetOrCreateStatus(property).PendingValues.Dequeue(); |
|||
} |
|||
|
|||
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); |
|||
private readonly DeferredSetter<TSetRecord> _setter; |
|||
|
|||
/// <summary>
|
|||
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
|
|||
/// </summary>
|
|||
/// <param name="property">The property to set.</param>
|
|||
/// <param name="backing">The backing field for the property</param>
|
|||
/// <param name="setterCallback">
|
|||
/// A callback that actually sets the property.
|
|||
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
|
|||
/// </param>
|
|||
/// <param name="value">The value to try to set.</param>
|
|||
public bool SetAndNotify<TValue>( |
|||
AvaloniaProperty property, |
|||
ref TValue backing, |
|||
SetterDelegate<TValue> setterCallback, |
|||
TSetRecord value) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(setterCallback != null); |
|||
if (!IsNotifying(property)) |
|||
internal NotifyDisposable(DeferredSetter<TSetRecord> setter) |
|||
{ |
|||
bool updated = false; |
|||
if (!object.Equals(value, backing)) |
|||
{ |
|||
updated = setterCallback(value, ref backing, notification => |
|||
{ |
|||
using (MarkNotifying(property)) |
|||
{ |
|||
notification(); |
|||
} |
|||
}); |
|||
} |
|||
while (HasPendingSet(property)) |
|||
{ |
|||
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => |
|||
{ |
|||
using (MarkNotifying(property)) |
|||
{ |
|||
notification(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
return updated; |
|||
_setter = setter; |
|||
_setter._isNotifying = true; |
|||
} |
|||
else if(!object.Equals(value, backing)) |
|||
|
|||
public void Dispose() |
|||
{ |
|||
AddPendingSet(property, value); |
|||
_setter._isNotifying = false; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,9 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
public interface IScrollAnchorProvider |
|||
{ |
|||
IControl CurrentAnchor { get; } |
|||
void RegisterAnchorCandidate(IControl element); |
|||
void UnregisterAnchorCandidate(IControl element); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// 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.
|
|||
|
|||
using Avalonia.Controls.Templates; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class ItemTemplateWrapper |
|||
{ |
|||
private readonly IDataTemplate _dataTemplate; |
|||
|
|||
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; |
|||
|
|||
public IControl GetElement(IControl parent, object data) |
|||
{ |
|||
var selectedTemplate = _dataTemplate; |
|||
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); |
|||
IControl element = null; |
|||
|
|||
if (recyclePool != null) |
|||
{ |
|||
// try to get an element from the recycle pool.
|
|||
element = recyclePool.TryGetElement(string.Empty, parent); |
|||
} |
|||
|
|||
if (element == null) |
|||
{ |
|||
// no element was found in recycle pool, create a new element
|
|||
element = selectedTemplate.Build(data); |
|||
|
|||
// Associate template with element
|
|||
element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
public void RecycleElement(IControl parent, IControl element) |
|||
{ |
|||
var selectedTemplate = _dataTemplate; |
|||
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); |
|||
if (recyclePool == null) |
|||
{ |
|||
// No Recycle pool in the template, create one.
|
|||
recyclePool = new RecyclePool(); |
|||
RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); |
|||
} |
|||
|
|||
recyclePool.PutElement(element, "" /* key */, parent); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,724 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a data-driven collection control that incorporates a flexible layout system,
|
|||
/// custom views, and virtualization.
|
|||
/// </summary>
|
|||
public class ItemsRepeater : Panel |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="HorizontalCacheLength"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<double> HorizontalCacheLengthProperty = |
|||
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(HorizontalCacheLength), 2.0); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemTemplate"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty = |
|||
ItemsControl.ItemTemplateProperty.AddOwner<ItemsRepeater>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Items"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ItemsRepeater, IEnumerable> ItemsProperty = |
|||
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Layout"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<AttachedLayout> LayoutProperty = |
|||
AvaloniaProperty.Register<ItemsRepeater, AttachedLayout>(nameof(Layout), new StackLayout()); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="VerticalCacheLength"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<double> VerticalCacheLengthProperty = |
|||
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0); |
|||
|
|||
private static readonly AttachedProperty<VirtualizationInfo> VirtualizationInfoProperty = |
|||
AvaloniaProperty.RegisterAttached<ItemsRepeater, IControl, VirtualizationInfo>("VirtualizationInfo"); |
|||
|
|||
internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); |
|||
internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); |
|||
|
|||
private readonly ViewManager _viewManager; |
|||
private readonly ViewportManager _viewportManager; |
|||
private IEnumerable _items; |
|||
private VirtualizingLayoutContext _layoutContext; |
|||
private NotifyCollectionChangedEventArgs _processingItemsSourceChange; |
|||
private bool _isLayoutInProgress; |
|||
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; |
|||
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; |
|||
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemsRepeater"/> class.
|
|||
/// </summary>
|
|||
public ItemsRepeater() |
|||
{ |
|||
_viewManager = new ViewManager(this); |
|||
_viewportManager = new ViewportManager(this); |
|||
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); |
|||
OnLayoutChanged(null, Layout); |
|||
} |
|||
|
|||
static ItemsRepeater() |
|||
{ |
|||
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the layout used to size and position elements in the ItemsRepeater.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The layout used to size and position elements. The default is a StackLayout with
|
|||
/// vertical orientation.
|
|||
/// </value>
|
|||
public AttachedLayout Layout |
|||
{ |
|||
get => GetValue(LayoutProperty); |
|||
set => SetValue(LayoutProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets an object source used to generate the content of the ItemsRepeater.
|
|||
/// </summary>
|
|||
public IEnumerable Items |
|||
{ |
|||
get => _items; |
|||
set => SetAndRaise(ItemsProperty, ref _items, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the template used to display each item.
|
|||
/// </summary>
|
|||
public IDataTemplate ItemTemplate |
|||
{ |
|||
get => GetValue(ItemTemplateProperty); |
|||
set => SetValue(ItemTemplateProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates the size of the buffer used to realize items when
|
|||
/// panning or scrolling horizontally.
|
|||
/// </summary>
|
|||
public double HorizontalCacheLength |
|||
{ |
|||
get => GetValue(HorizontalCacheLengthProperty); |
|||
set => SetValue(HorizontalCacheLengthProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates the size of the buffer used to realize items when
|
|||
/// panning or scrolling vertically.
|
|||
/// </summary>
|
|||
public double VerticalCacheLength |
|||
{ |
|||
get => GetValue(VerticalCacheLengthProperty); |
|||
set => SetValue(VerticalCacheLengthProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a standardized view of the supported interactions between a given Items object and
|
|||
/// the ItemsRepeater control and its associated components.
|
|||
/// </summary>
|
|||
public ItemsSourceView ItemsSourceView { get; private set; } |
|||
|
|||
internal ItemTemplateWrapper ItemTemplateShim { get; set; } |
|||
internal Point LayoutOrigin { get; set; } |
|||
internal object LayoutState { get; set; } |
|||
internal IControl MadeAnchor => _viewportManager.MadeAnchor; |
|||
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); |
|||
internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; |
|||
|
|||
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; |
|||
|
|||
private LayoutContext LayoutContext |
|||
{ |
|||
get |
|||
{ |
|||
if (_layoutContext == null) |
|||
{ |
|||
_layoutContext = new RepeaterLayoutContext(this); |
|||
} |
|||
|
|||
return _layoutContext; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Occurs each time an element is cleared and made available to be re-used.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This event is raised immediately each time an element is cleared, such as when it falls
|
|||
/// outside the range of realized items. Elements are cleared when they become available
|
|||
/// for re-use.
|
|||
/// </remarks>
|
|||
public event EventHandler<ItemsRepeaterElementClearingEventArgs> ElementClearing; |
|||
|
|||
/// <summary>
|
|||
/// Occurs for each realized <see cref="IControl"/> when the index for the item it
|
|||
/// represents has changed.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// When you use ItemsRepeater to build a more complex control that supports specific
|
|||
/// interactions on the child elements (such as selection or click), it is useful to be
|
|||
/// able to keep an up-to-date identifier for the backing data item.
|
|||
///
|
|||
/// This event is raised for each realized IControl where the index for the item it
|
|||
/// represents has changed. For example, when another item is added or removed in the data
|
|||
/// source, the index for items that come after in the ordering will be impacted.
|
|||
/// </remarks>
|
|||
public event EventHandler<ItemsRepeaterElementIndexChangedEventArgs> ElementIndexChanged; |
|||
|
|||
/// <summary>
|
|||
/// Occurs each time an element is prepared for use.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The prepared element might be newly created or an existing element that is being re-
|
|||
/// used.
|
|||
/// </remarks>
|
|||
public event EventHandler<ItemsRepeaterElementPreparedEventArgs> ElementPrepared; |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the index of the item from the data source that corresponds to the specified
|
|||
/// <see cref="IControl"/>.
|
|||
/// </summary>
|
|||
/// <param name="element">
|
|||
/// The element that corresponds to the item to get the index of.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The index of the item from the data source that corresponds to the specified UIElement,
|
|||
/// or -1 if the element is not supported.
|
|||
/// </returns>
|
|||
public int GetElementIndex(IControl element) => GetElementIndexImpl(element); |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the realized UIElement that corresponds to the item at the specified index in
|
|||
/// the data source.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
/// <returns>
|
|||
/// he UIElement that corresponds to the item at the specified index if the item is
|
|||
/// realized, or null if the item is not realized.
|
|||
/// </returns>
|
|||
public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); |
|||
|
|||
internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); |
|||
|
|||
internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); |
|||
|
|||
internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); |
|||
|
|||
internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) |
|||
{ |
|||
var value = element.GetValue(VirtualizationInfoProperty); |
|||
return value; |
|||
} |
|||
|
|||
internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) |
|||
{ |
|||
if (TryGetVirtualizationInfo(element) != null) |
|||
{ |
|||
throw new InvalidOperationException("VirtualizationInfo already created."); |
|||
} |
|||
|
|||
var result = new VirtualizationInfo(); |
|||
element.SetValue(VirtualizationInfoProperty, result); |
|||
return result; |
|||
} |
|||
|
|||
internal static VirtualizationInfo GetVirtualizationInfo(IControl element) |
|||
{ |
|||
var result = element.GetValue(VirtualizationInfoProperty); |
|||
|
|||
if (result == null) |
|||
{ |
|||
result = new VirtualizationInfo(); |
|||
element.SetValue(VirtualizationInfoProperty, result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
if (_isLayoutInProgress) |
|||
{ |
|||
throw new AvaloniaInternalException("Reentrancy detected during layout."); |
|||
} |
|||
|
|||
if (IsProcessingCollectionChange) |
|||
{ |
|||
throw new NotSupportedException("Cannot run layout in the middle of a collection change."); |
|||
} |
|||
|
|||
_viewportManager.OnOwnerMeasuring(); |
|||
|
|||
_isLayoutInProgress = true; |
|||
|
|||
try |
|||
{ |
|||
_viewManager.PrunePinnedElements(); |
|||
var extent = new Rect(); |
|||
var desiredSize = new Size(); |
|||
var layout = Layout; |
|||
|
|||
if (layout != null) |
|||
{ |
|||
var layoutContext = GetLayoutContext(); |
|||
|
|||
desiredSize = layout.Measure(layoutContext, availableSize); |
|||
extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); |
|||
|
|||
// Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
|
|||
// call GetElementAt(index).
|
|||
foreach (var element in Children) |
|||
{ |
|||
var virtInfo = GetVirtualizationInfo(element); |
|||
|
|||
if (virtInfo.Owner == ElementOwner.Layout && |
|||
virtInfo.AutoRecycleCandidate && |
|||
!virtInfo.KeepAlive) |
|||
{ |
|||
ClearElementImpl(element); |
|||
} |
|||
} |
|||
} |
|||
|
|||
_viewportManager.SetLayoutExtent(extent); |
|||
return desiredSize; |
|||
} |
|||
finally |
|||
{ |
|||
_isLayoutInProgress = false; |
|||
} |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
if (_isLayoutInProgress) |
|||
{ |
|||
throw new AvaloniaInternalException("Reentrancy detected during layout."); |
|||
} |
|||
|
|||
if (IsProcessingCollectionChange) |
|||
{ |
|||
throw new NotSupportedException("Cannot run layout in the middle of a collection change."); |
|||
} |
|||
|
|||
_isLayoutInProgress = true; |
|||
|
|||
try |
|||
{ |
|||
var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; |
|||
|
|||
// The view manager might clear elements during this call.
|
|||
// That's why we call it before arranging cleared elements
|
|||
// off screen.
|
|||
_viewManager.OnOwnerArranged(); |
|||
|
|||
foreach (var element in Children) |
|||
{ |
|||
var virtInfo = GetVirtualizationInfo(element); |
|||
virtInfo.KeepAlive = false; |
|||
|
|||
if (virtInfo.Owner == ElementOwner.ElementFactory || |
|||
virtInfo.Owner == ElementOwner.PinnedPool) |
|||
{ |
|||
// Toss it away. And arrange it with size 0 so that XYFocus won't use it.
|
|||
element.Arrange(new Rect( |
|||
ClearedElementsArrangePosition.X - element.DesiredSize.Width, |
|||
ClearedElementsArrangePosition.Y - element.DesiredSize.Height, |
|||
0, |
|||
0)); |
|||
} |
|||
else |
|||
{ |
|||
var newBounds = element.Bounds; |
|||
virtInfo.ArrangeBounds = newBounds; |
|||
} |
|||
} |
|||
|
|||
_viewportManager.OnOwnerArranged(); |
|||
|
|||
return arrangeSize; |
|||
} |
|||
finally |
|||
{ |
|||
_isLayoutInProgress = false; |
|||
} |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
InvalidateMeasure(); |
|||
_viewportManager.ResetScrollers(); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
_viewportManager.ResetScrollers(); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) |
|||
{ |
|||
var property = args.Property; |
|||
|
|||
if (property == ItemsProperty) |
|||
{ |
|||
var newValue = (IEnumerable)args.NewValue; |
|||
var newDataSource = newValue as ItemsSourceView; |
|||
if (newValue != null && newDataSource == null) |
|||
{ |
|||
newDataSource = new ItemsSourceView(newValue); |
|||
} |
|||
|
|||
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); |
|||
} |
|||
else if (property == ItemTemplateProperty) |
|||
{ |
|||
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); |
|||
} |
|||
else if (property == LayoutProperty) |
|||
{ |
|||
OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); |
|||
} |
|||
else if (property == HorizontalCacheLengthProperty) |
|||
{ |
|||
_viewportManager.HorizontalCacheLength = (double)args.NewValue; |
|||
} |
|||
else if (property == VerticalCacheLengthProperty) |
|||
{ |
|||
_viewportManager.VerticalCacheLength = (double)args.NewValue; |
|||
} |
|||
else |
|||
{ |
|||
base.OnPropertyChanged(args); |
|||
} |
|||
} |
|||
|
|||
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) |
|||
{ |
|||
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); |
|||
return element; |
|||
} |
|||
|
|||
internal void ClearElementImpl(IControl element) |
|||
{ |
|||
// Clearing an element due to a collection change
|
|||
// is more strict in that pinned elements will be forcibly
|
|||
// unpinned and sent back to the view generator.
|
|||
var isClearedDueToCollectionChange = |
|||
_processingItemsSourceChange != null && |
|||
(_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || |
|||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || |
|||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); |
|||
|
|||
_viewManager.ClearElement(element, isClearedDueToCollectionChange); |
|||
_viewportManager.OnElementCleared(element); |
|||
} |
|||
|
|||
private int GetElementIndexImpl(IControl element) |
|||
{ |
|||
var virtInfo = TryGetVirtualizationInfo(element); |
|||
return _viewManager.GetElementIndex(virtInfo); |
|||
} |
|||
|
|||
private IControl GetElementFromIndexImpl(int index) |
|||
{ |
|||
IControl result = null; |
|||
|
|||
var children = Children; |
|||
for (var i = 0; i < children.Count && result == null; ++i) |
|||
{ |
|||
var element = children[i]; |
|||
var virtInfo = TryGetVirtualizationInfo(element); |
|||
if (virtInfo?.IsRealized == true && virtInfo.Index == index) |
|||
{ |
|||
result = element; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private IControl GetOrCreateElementImpl(int index) |
|||
{ |
|||
if (index >= 0 && index >= ItemsSourceView.Count) |
|||
{ |
|||
throw new ArgumentException("Argument index is invalid.", "index"); |
|||
} |
|||
|
|||
if (_isLayoutInProgress) |
|||
{ |
|||
throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); |
|||
} |
|||
|
|||
var element = GetElementFromIndexImpl(index); |
|||
bool isAnchorOutsideRealizedRange = element == null; |
|||
|
|||
if (isAnchorOutsideRealizedRange) |
|||
{ |
|||
if (Layout == null) |
|||
{ |
|||
throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); |
|||
} |
|||
|
|||
element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); |
|||
element.Measure(Size.Infinity); |
|||
} |
|||
|
|||
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); |
|||
InvalidateMeasure(); |
|||
|
|||
return element; |
|||
} |
|||
|
|||
internal void OnElementPrepared(IControl element, int index) |
|||
{ |
|||
_viewportManager.OnElementPrepared(element); |
|||
if (ElementPrepared != null) |
|||
{ |
|||
if (_elementPreparedArgs == null) |
|||
{ |
|||
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); |
|||
} |
|||
else |
|||
{ |
|||
_elementPreparedArgs.Update(element, index); |
|||
} |
|||
|
|||
ElementPrepared(this, _elementPreparedArgs); |
|||
} |
|||
} |
|||
|
|||
internal void OnElementClearing(IControl element) |
|||
{ |
|||
if (ElementClearing != null) |
|||
{ |
|||
if (_elementClearingArgs == null) |
|||
{ |
|||
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); |
|||
} |
|||
else |
|||
{ |
|||
_elementClearingArgs.Update(element); |
|||
} |
|||
|
|||
ElementClearing(this, _elementClearingArgs); |
|||
} |
|||
} |
|||
|
|||
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) |
|||
{ |
|||
if (ElementIndexChanged != null) |
|||
{ |
|||
if (_elementIndexChangedArgs == null) |
|||
{ |
|||
_elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); |
|||
} |
|||
else |
|||
{ |
|||
_elementIndexChangedArgs.Update(element, oldIndex, newIndex); |
|||
} |
|||
|
|||
ElementIndexChanged(this, _elementIndexChangedArgs); |
|||
} |
|||
} |
|||
|
|||
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) |
|||
{ |
|||
if (_isLayoutInProgress) |
|||
{ |
|||
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); |
|||
} |
|||
|
|||
ItemsSourceView?.Dispose(); |
|||
ItemsSourceView = newValue; |
|||
|
|||
if (oldValue != null) |
|||
{ |
|||
oldValue.CollectionChanged -= OnItemsSourceViewChanged; |
|||
} |
|||
|
|||
if (newValue != null) |
|||
{ |
|||
newValue.CollectionChanged += OnItemsSourceViewChanged; |
|||
} |
|||
|
|||
if (Layout != null) |
|||
{ |
|||
if (Layout is VirtualizingLayout virtualLayout) |
|||
{ |
|||
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); |
|||
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); |
|||
} |
|||
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) |
|||
{ |
|||
if (GetVirtualizationInfo(element).IsRealized) |
|||
{ |
|||
ClearElementImpl(element); |
|||
} |
|||
} |
|||
} |
|||
|
|||
InvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) |
|||
{ |
|||
if (_isLayoutInProgress && oldValue != null) |
|||
{ |
|||
throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); |
|||
} |
|||
|
|||
// Since the ItemTemplate has changed, we need to re-evaluate all the items that
|
|||
// have already been created and are now in the tree. The easiest way to do that
|
|||
// would be to do a reset.. Note that this has to be done before we change the template
|
|||
// so that the cleared elements go back into the old template.
|
|||
if (Layout != null) |
|||
{ |
|||
if (Layout is VirtualizingLayout virtualLayout) |
|||
{ |
|||
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); |
|||
_processingItemsSourceChange = args; |
|||
|
|||
try |
|||
{ |
|||
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); |
|||
} |
|||
finally |
|||
{ |
|||
_processingItemsSourceChange = null; |
|||
} |
|||
} |
|||
else if (Layout is NonVirtualizingLayout) |
|||
{ |
|||
// Walk through all the elements and make sure they are cleared for
|
|||
// non-virtualizing layouts.
|
|||
foreach (var element in Children) |
|||
{ |
|||
if (GetVirtualizationInfo(element).IsRealized) |
|||
{ |
|||
ClearElementImpl(element); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
ItemTemplateShim = new ItemTemplateWrapper(newValue); |
|||
|
|||
InvalidateMeasure(); |
|||
} |
|||
|
|||
private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) |
|||
{ |
|||
if (_isLayoutInProgress) |
|||
{ |
|||
throw new InvalidOperationException("Layout cannot be changed during layout."); |
|||
} |
|||
|
|||
_viewManager.OnLayoutChanging(); |
|||
|
|||
if (oldValue != null) |
|||
{ |
|||
oldValue.UninitializeForContext(LayoutContext); |
|||
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; |
|||
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; |
|||
|
|||
// Walk through all the elements and make sure they are cleared
|
|||
foreach (var element in Children) |
|||
{ |
|||
if (GetVirtualizationInfo(element).IsRealized) |
|||
{ |
|||
ClearElementImpl(element); |
|||
} |
|||
} |
|||
|
|||
LayoutState = null; |
|||
} |
|||
|
|||
if (newValue != null) |
|||
{ |
|||
newValue.InitializeForContext(LayoutContext); |
|||
newValue.MeasureInvalidated += InvalidateMeasureForLayout; |
|||
newValue.ArrangeInvalidated += InvalidateArrangeForLayout; |
|||
} |
|||
|
|||
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; |
|||
_viewportManager.OnLayoutChanged(isVirtualizingLayout); |
|||
InvalidateMeasure(); |
|||
} |
|||
|
|||
private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
if (_isLayoutInProgress) |
|||
{ |
|||
// Bad things will follow if the data changes while we are in the middle of a layout pass.
|
|||
throw new InvalidOperationException("Changes in data source are not allowed during layout."); |
|||
} |
|||
|
|||
if (IsProcessingCollectionChange) |
|||
{ |
|||
throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); |
|||
} |
|||
|
|||
_processingItemsSourceChange = args; |
|||
|
|||
try |
|||
{ |
|||
_viewManager.OnItemsSourceChanged(sender, args); |
|||
|
|||
if (Layout != null) |
|||
{ |
|||
if (Layout is VirtualizingLayout virtualLayout) |
|||
{ |
|||
virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); |
|||
} |
|||
else |
|||
{ |
|||
// NonVirtualizingLayout
|
|||
InvalidateMeasure(); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_processingItemsSourceChange = null; |
|||
} |
|||
} |
|||
|
|||
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure(); |
|||
|
|||
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange(); |
|||
|
|||
private VirtualizingLayoutContext GetLayoutContext() |
|||
{ |
|||
if (_layoutContext == null) |
|||
{ |
|||
_layoutContext = new RepeaterLayoutContext(this); |
|||
} |
|||
|
|||
return _layoutContext; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="ItemsRepeater.ElementClearing"/> event.
|
|||
/// </summary>
|
|||
public class ItemsRepeaterElementClearingEventArgs : EventArgs |
|||
{ |
|||
internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; |
|||
|
|||
/// <summary>
|
|||
/// Gets the element that is being cleared for re-use.
|
|||
/// </summary>
|
|||
public IControl Element { get; private set; } |
|||
|
|||
internal void Update(IControl element) => Element = element; |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="ItemsRepeater.ElementIndexChanged"/> event.
|
|||
/// </summary>
|
|||
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs |
|||
{ |
|||
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) |
|||
{ |
|||
Element = element; |
|||
NewIndex = newIndex; |
|||
OldIndex = oldIndex; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the element for which the index changed.
|
|||
/// </summary>
|
|||
public IControl Element { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the element after the change.
|
|||
/// </summary>
|
|||
public int NewIndex { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the element before the change.
|
|||
/// </summary>
|
|||
public int OldIndex { get; private set; } |
|||
|
|||
internal void Update(IControl element, int newIndex, int oldIndex) |
|||
{ |
|||
Element = element; |
|||
NewIndex = newIndex; |
|||
OldIndex = oldIndex; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// 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.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="ItemsRepeater.ElementPrepared"/> event.
|
|||
/// </summary>
|
|||
public class ItemsRepeaterElementPreparedEventArgs |
|||
{ |
|||
internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) |
|||
{ |
|||
Element = element; |
|||
Index = index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the prepared element.
|
|||
/// </summary>
|
|||
public IControl Element { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the item the element was prepared for.
|
|||
/// </summary>
|
|||
public int Index { get; private set; } |
|||
|
|||
internal void Update(IControl element, int index) |
|||
{ |
|||
Element = element; |
|||
Index = index; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a standardized view of the supported interactions between a given ItemsSource
|
|||
/// object and an <see cref="ItemsRepeater"/> control.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Components written to work with ItemsRepeater should consume the
|
|||
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
|
|||
/// view of the Items. That way, each component does not need to know if the source is an
|
|||
/// IEnumerable, an IList, or something else.
|
|||
/// </remarks>
|
|||
public class ItemsSourceView : INotifyCollectionChanged, IDisposable |
|||
{ |
|||
private readonly IList _inner; |
|||
private INotifyCollectionChanged _notifyCollectionChanged; |
|||
private int _cachedSize = -1; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
|
|||
/// </summary>
|
|||
/// <param name="source">The data source.</param>
|
|||
public ItemsSourceView(IEnumerable source) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(source != null); |
|||
|
|||
_inner = source as IList; |
|||
|
|||
if (_inner == null && source is IEnumerable<object> objectEnumerable) |
|||
{ |
|||
_inner = new List<object>(objectEnumerable); |
|||
} |
|||
else |
|||
{ |
|||
_inner = new List<object>(source.Cast<object>()); |
|||
} |
|||
|
|||
ListenToCollectionChanges(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items in the collection.
|
|||
/// </summary>
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_cachedSize == -1) |
|||
{ |
|||
_cachedSize = _inner.Count; |
|||
} |
|||
|
|||
return _cachedSize; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </remarks>
|
|||
public bool HasKeyIndexMapping => false; |
|||
|
|||
/// <summary>
|
|||
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
|
|||
/// </summary>
|
|||
public event NotifyCollectionChangedEventHandler CollectionChanged; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (_notifyCollectionChanged != null) |
|||
{ |
|||
_notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the item at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index.</param>
|
|||
/// <returns>the item.</returns>
|
|||
public object GetAt(int index) => _inner[index]; |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the index of the item that has the specified unique identifier (key).
|
|||
/// </summary>
|
|||
/// <param name="index">The index.</param>
|
|||
/// <returns>The key</returns>
|
|||
/// <remarks>
|
|||
/// TODO: Not yet implemented in Avalonia.
|
|||
/// </remarks>
|
|||
public string KeyFromIndex(int index) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the unique identifier (key) for the item at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="key">The key.</param>
|
|||
/// <returns>The index.</returns>
|
|||
/// <remarks>
|
|||
/// TODO: Not yet implemented in Avalonia.
|
|||
/// </remarks>
|
|||
public int IndexFromKey(string key) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
_cachedSize = _inner.Count; |
|||
CollectionChanged?.Invoke(this, args); |
|||
} |
|||
|
|||
private void ListenToCollectionChanges() |
|||
{ |
|||
if (_inner is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged += OnCollectionChanged; |
|||
_notifyCollectionChanged = incc; |
|||
} |
|||
} |
|||
|
|||
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
OnItemsSourceChanged(e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Controls.Templates; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class RecyclePool |
|||
{ |
|||
public static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty = |
|||
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool)); |
|||
|
|||
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>(); |
|||
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>(); |
|||
|
|||
public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate) |
|||
{ |
|||
s_pools.TryGetValue(dataTemplate, out var result); |
|||
return result; |
|||
} |
|||
|
|||
public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value); |
|||
|
|||
public void PutElement(IControl element, string key, IControl owner) |
|||
{ |
|||
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); |
|||
var elementInfo = new ElementInfo(element, ownerAsPanel); |
|||
|
|||
if (!_elements.TryGetValue(key, out var pool)) |
|||
{ |
|||
pool = new List<ElementInfo>(); |
|||
_elements.Add(key, pool); |
|||
} |
|||
|
|||
pool.Add(elementInfo); |
|||
} |
|||
|
|||
public IControl TryGetElement(string key, IControl owner) |
|||
{ |
|||
if (_elements.TryGetValue(key, out var elements)) |
|||
{ |
|||
if (elements.Count > 0) |
|||
{ |
|||
// Prefer an element from the same owner or with no owner so that we don't incur
|
|||
// the enter/leave cost during recycling.
|
|||
// TODO: prioritize elements with the same owner to those without an owner.
|
|||
var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault(); |
|||
elements.Remove(elementInfo); |
|||
|
|||
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); |
|||
if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel) |
|||
{ |
|||
// Element is still under its parent. remove it from its parent.
|
|||
var panel = elementInfo.Owner; |
|||
if (panel != null) |
|||
{ |
|||
int childIndex = panel.Children.IndexOf(elementInfo.Element); |
|||
if (childIndex == -1) |
|||
{ |
|||
throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection."); |
|||
} |
|||
|
|||
panel.Children.RemoveAt(childIndex); |
|||
} |
|||
} |
|||
|
|||
return elementInfo.Element; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private IPanel EnsureOwnerIsPanelOrNull(IControl owner) |
|||
{ |
|||
if (owner is IPanel panel) |
|||
{ |
|||
return panel; |
|||
} |
|||
else if (owner != null) |
|||
{ |
|||
throw new InvalidOperationException("Owner must be IPanel or null."); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private class ElementInfo |
|||
{ |
|||
public ElementInfo(IControl element, IPanel owner) |
|||
{ |
|||
Element = element; |
|||
Owner = owner; |
|||
} |
|||
|
|||
public IControl Element { get; } |
|||
public IPanel Owner { get;} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class RepeaterLayoutContext : VirtualizingLayoutContext |
|||
{ |
|||
private readonly ItemsRepeater _owner; |
|||
|
|||
public RepeaterLayoutContext(ItemsRepeater owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
protected override Point LayoutOriginCore |
|||
{ |
|||
get => _owner.LayoutOrigin; |
|||
set => _owner.LayoutOrigin = value; |
|||
} |
|||
|
|||
protected override object LayoutStateCore |
|||
{ |
|||
get => _owner.LayoutState; |
|||
set => _owner.LayoutState = value; |
|||
} |
|||
|
|||
protected override int RecommendedAnchorIndexCore |
|||
{ |
|||
get |
|||
{ |
|||
int anchorIndex = -1; |
|||
var anchor = _owner.SuggestedAnchor; |
|||
if (anchor != null) |
|||
{ |
|||
anchorIndex = _owner.GetElementIndex(anchor); |
|||
} |
|||
|
|||
return anchorIndex; |
|||
} |
|||
} |
|||
|
|||
protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; |
|||
|
|||
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) |
|||
{ |
|||
return _owner.GetElementImpl( |
|||
index, |
|||
(options & ElementRealizationOptions.ForceCreate) != 0, |
|||
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0); |
|||
} |
|||
|
|||
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); |
|||
|
|||
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); |
|||
|
|||
protected override Rect RealizationRectCore() => _owner.RealizationWindow; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class UniqueIdElementPool : IEnumerable<KeyValuePair<string, IControl>> |
|||
{ |
|||
private readonly Dictionary<string, IControl> _elementMap = new Dictionary<string, IControl>(); |
|||
private readonly ItemsRepeater _owner; |
|||
|
|||
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner; |
|||
|
|||
public void Add(IControl element) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var key = virtInfo.UniqueId; |
|||
|
|||
if (_elementMap.ContainsKey(key)) |
|||
{ |
|||
throw new InvalidOperationException($"The unique id provided ({key}) is not unique."); |
|||
} |
|||
|
|||
_elementMap.Add(key, element); |
|||
} |
|||
|
|||
public IControl Remove(int index) |
|||
{ |
|||
// Check if there is already a element in the mapping and if so, use it.
|
|||
string key = _owner.ItemsSourceView.KeyFromIndex(index); |
|||
|
|||
if (_elementMap.TryGetValue(key, out var element)) |
|||
{ |
|||
_elementMap.Remove(key); |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
_elementMap.Clear(); |
|||
} |
|||
|
|||
public IEnumerator<KeyValuePair<string, IControl>> GetEnumerator() => _elementMap.GetEnumerator(); |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,682 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal sealed class ViewManager |
|||
{ |
|||
private const int FirstRealizedElementIndexDefault = int.MaxValue; |
|||
private const int LastRealizedElementIndexDefault = int.MinValue; |
|||
|
|||
private readonly ItemsRepeater _owner; |
|||
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>(); |
|||
private readonly UniqueIdElementPool _resetPool; |
|||
private IControl _lastFocusedElement; |
|||
private bool _isDataSourceStableResetPending; |
|||
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; |
|||
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; |
|||
private bool _eventsSubscribed; |
|||
|
|||
public ViewManager(ItemsRepeater owner) |
|||
{ |
|||
_owner = owner; |
|||
_resetPool = new UniqueIdElementPool(owner); |
|||
} |
|||
|
|||
public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle) |
|||
{ |
|||
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index); |
|||
if (element == null) |
|||
{ |
|||
// check if this is the anchor made through repeater in preparation
|
|||
// for a bring into view.
|
|||
var madeAnchor = _owner.MadeAnchor; |
|||
if (madeAnchor != null) |
|||
{ |
|||
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); |
|||
if (anchorVirtInfo.Index == index) |
|||
{ |
|||
element = madeAnchor; |
|||
} |
|||
} |
|||
} |
|||
if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; |
|||
if (element == null) { element = GetElementFromPinnedElements(index); } |
|||
if (element == null) { element = GetElementFromElementFactory(index); } |
|||
|
|||
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); |
|||
if (suppressAutoRecycle) |
|||
{ |
|||
virtInfo.AutoRecycleCandidate = false; |
|||
} |
|||
else |
|||
{ |
|||
virtInfo.AutoRecycleCandidate = true; |
|||
virtInfo.KeepAlive = true; |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
public void ClearElement(IControl element, bool isClearedDueToCollectionChange) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var index = virtInfo.Index; |
|||
bool cleared = |
|||
ClearElementToUniqueIdResetPool(element, virtInfo) || |
|||
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); |
|||
|
|||
if (!cleared) |
|||
{ |
|||
ClearElementToElementFactory(element); |
|||
} |
|||
|
|||
// Both First and Last indices need to be valid or default.
|
|||
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout) |
|||
{ |
|||
// First and last were pointing to the same element and that is going away.
|
|||
InvalidateRealizedIndicesHeldByLayout(); |
|||
} |
|||
else if (index == _firstRealizedElementIndexHeldByLayout) |
|||
{ |
|||
// The FirstElement is going away, shrink the range by one.
|
|||
++_firstRealizedElementIndexHeldByLayout; |
|||
} |
|||
else if (index == _lastRealizedElementIndexHeldByLayout) |
|||
{ |
|||
// Last element is going away, shrink the range by one at the end.
|
|||
--_lastRealizedElementIndexHeldByLayout; |
|||
} |
|||
else |
|||
{ |
|||
// Index is either outside the range we are keeping track of or inside the range.
|
|||
// In both these cases, we just keep the range we have. If this clear was due to
|
|||
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
|
|||
} |
|||
} |
|||
|
|||
public void ClearElementToElementFactory(IControl element) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var clearedIndex = virtInfo.Index; |
|||
_owner.OnElementClearing(element); |
|||
_owner.ItemTemplateShim.RecycleElement(_owner, element); |
|||
|
|||
virtInfo.MoveOwnershipToElementFactory(); |
|||
|
|||
if (_lastFocusedElement == element) |
|||
{ |
|||
// 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); |
|||
} |
|||
|
|||
} |
|||
|
|||
private void MoveFocusFromClearedIndex(int clearedIndex) |
|||
{ |
|||
IControl focusedChild = null; |
|||
var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); |
|||
if (focusCandidate != null) |
|||
{ |
|||
focusCandidate.Focus(); |
|||
_lastFocusedElement = focusedChild; |
|||
|
|||
// Add pin to hold the focused element.
|
|||
UpdatePin(focusedChild, true /* addPin */); |
|||
} |
|||
else |
|||
{ |
|||
// We could not find a candiate.
|
|||
_lastFocusedElement = null; |
|||
} |
|||
} |
|||
|
|||
IControl FindFocusCandidate(int clearedIndex, IControl focusedChild) |
|||
{ |
|||
// Walk through all the children and find elements with index before and after the cleared index.
|
|||
// Note that during a delete the next element would now have the same index.
|
|||
int previousIndex = int.MinValue; |
|||
int nextIndex = int.MaxValue; |
|||
IControl nextElement = null; |
|||
IControl previousElement = null; |
|||
|
|||
foreach (var child in _owner.Children) |
|||
{ |
|||
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); |
|||
if (virtInfo?.IsHeldByLayout == true) |
|||
{ |
|||
int currentIndex = virtInfo.Index; |
|||
if (currentIndex < clearedIndex) |
|||
{ |
|||
if (currentIndex > previousIndex) |
|||
{ |
|||
previousIndex = currentIndex; |
|||
previousElement = child; |
|||
} |
|||
} |
|||
else if (currentIndex >= clearedIndex) |
|||
{ |
|||
// Note that we use >= above because if we deleted the focused element,
|
|||
// the next element would have the same index now.
|
|||
if (currentIndex < nextIndex) |
|||
{ |
|||
nextIndex = currentIndex; |
|||
nextElement = child; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// TODO: Find the next element if one exists, if not use the previous element.
|
|||
// If the container itself is not focusable, find a descendent that is.
|
|||
|
|||
return nextElement; |
|||
} |
|||
|
|||
public int GetElementIndex(VirtualizationInfo virtInfo) |
|||
{ |
|||
if (virtInfo == null) |
|||
{ |
|||
throw new ArgumentException("Element is not a child of this ItemsRepeater."); |
|||
} |
|||
|
|||
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; |
|||
} |
|||
|
|||
public void PrunePinnedElements() |
|||
{ |
|||
EnsureEventSubscriptions(); |
|||
|
|||
// Go through pinned elements and make sure they still have
|
|||
// a reason to be pinned.
|
|||
for (var i = 0; i < _pinnedPool.Count; ++i) |
|||
{ |
|||
var elementInfo = _pinnedPool[i]; |
|||
var virtInfo = elementInfo.VirtualizationInfo; |
|||
|
|||
if (!virtInfo.IsPinned) |
|||
{ |
|||
_pinnedPool.RemoveAt(i); |
|||
--i; |
|||
|
|||
// Pinning was the only thing keeping this element alive.
|
|||
ClearElementToElementFactory(elementInfo.PinnedElement); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void UpdatePin(IControl element, bool addPin) |
|||
{ |
|||
var parent = element.VisualParent; |
|||
var child = (IVisual)element; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
if (parent is ItemsRepeater repeater) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child); |
|||
if (virtInfo.IsRealized) |
|||
{ |
|||
if (addPin) |
|||
{ |
|||
virtInfo.AddPin(); |
|||
} |
|||
else if (virtInfo.IsPinned) |
|||
{ |
|||
if (virtInfo.RemovePin() == 0) |
|||
{ |
|||
// ElementFactory is invoked during the measure pass.
|
|||
// We will clear the element then.
|
|||
repeater.InvalidateMeasure(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
child = parent; |
|||
parent = child.VisualParent; |
|||
} |
|||
} |
|||
|
|||
public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
// Note: For items that have been removed, the index will not be touched. It will hold
|
|||
// the old index before it was removed. It is not valid anymore.
|
|||
switch (args.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
var newIndex = args.NewStartingIndex; |
|||
var newCount = args.NewItems.Count; |
|||
EnsureFirstLastRealizedIndices(); |
|||
if (newIndex <= _lastRealizedElementIndexHeldByLayout) |
|||
{ |
|||
_lastRealizedElementIndexHeldByLayout += newCount; |
|||
foreach (var element in _owner.Children) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var dataIndex = virtInfo.Index; |
|||
|
|||
if (virtInfo.IsRealized && dataIndex >= newIndex) |
|||
{ |
|||
UpdateElementIndex(element, virtInfo, dataIndex + newCount); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Indices held by layout are not affected
|
|||
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
|
|||
for (var i = 0; i < _pinnedPool.Count; ++i) |
|||
{ |
|||
var elementInfo = _pinnedPool[i]; |
|||
var virtInfo = elementInfo.VirtualizationInfo; |
|||
var dataIndex = virtInfo.Index; |
|||
|
|||
if (virtInfo.IsRealized && dataIndex >= newIndex) |
|||
{ |
|||
var element = elementInfo.PinnedElement; |
|||
UpdateElementIndex(element, virtInfo, dataIndex + newCount); |
|||
} |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
|
|||
// Two cases here
|
|||
// case 1: oldCount == newCount
|
|||
// indices are not affected. nothing to do here.
|
|||
// case 2: oldCount != newCount
|
|||
// Replaced with less or more items. This is like an insert or remove
|
|||
// depending on the counts.
|
|||
var oldStartIndex = args.OldStartingIndex; |
|||
var newStartingIndex = args.NewStartingIndex; |
|||
var oldCount = args.OldItems.Count; |
|||
var newCount = args.NewItems.Count; |
|||
if (oldStartIndex != newStartingIndex) |
|||
{ |
|||
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); |
|||
} |
|||
|
|||
if (oldCount == 0) |
|||
{ |
|||
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); |
|||
} |
|||
|
|||
if (newCount == 0) |
|||
{ |
|||
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); |
|||
} |
|||
|
|||
int countChange = newCount - oldCount; |
|||
if (countChange != 0) |
|||
{ |
|||
// countChange > 0 : countChange items were added
|
|||
// countChange < 0 : -countChange items were removed
|
|||
foreach (var element in _owner.Children) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var dataIndex = virtInfo.Index; |
|||
|
|||
if (virtInfo.IsRealized) |
|||
{ |
|||
if (dataIndex >= oldStartIndex + oldCount) |
|||
{ |
|||
UpdateElementIndex(element, virtInfo, dataIndex + countChange); |
|||
} |
|||
} |
|||
} |
|||
|
|||
EnsureFirstLastRealizedIndices(); |
|||
_lastRealizedElementIndexHeldByLayout += countChange; |
|||
} |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
var oldStartIndex = args.OldStartingIndex; |
|||
var oldCount = args.OldItems.Count; |
|||
foreach (var element in _owner.Children) |
|||
{ |
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
var dataIndex = virtInfo.Index; |
|||
|
|||
if (virtInfo.IsRealized) |
|||
{ |
|||
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) |
|||
{ |
|||
// If we are doing the mapping, remove the element who's data was removed.
|
|||
_owner.ClearElementImpl(element); |
|||
} |
|||
else if (dataIndex >= (oldStartIndex + oldCount)) |
|||
{ |
|||
UpdateElementIndex(element, virtInfo, dataIndex - oldCount); |
|||
} |
|||
} |
|||
} |
|||
|
|||
InvalidateRealizedIndicesHeldByLayout(); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
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) |
|||
{ |
|||
_owner.ClearElementImpl(element); |
|||
} |
|||
} |
|||
|
|||
InvalidateRealizedIndicesHeldByLayout(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private void EnsureFirstLastRealizedIndices() |
|||
{ |
|||
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault) |
|||
{ |
|||
// This will ensure that the indexes are updated.
|
|||
GetElementIfAlreadyHeldByLayout(0); |
|||
} |
|||
} |
|||
|
|||
public void OnLayoutChanging() |
|||
{ |
|||
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true) |
|||
{ |
|||
_isDataSourceStableResetPending = true; |
|||
} |
|||
} |
|||
|
|||
public void OnOwnerArranged() |
|||
{ |
|||
if (_isDataSourceStableResetPending) |
|||
{ |
|||
_isDataSourceStableResetPending = false; |
|||
|
|||
foreach (var entry in _resetPool) |
|||
{ |
|||
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
|
|||
// Focused element is getting cleared. Need to figure out semantics on where
|
|||
// focus should go when the focused element is removed from the data collection.
|
|||
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */); |
|||
} |
|||
|
|||
_resetPool.Clear(); |
|||
} |
|||
} |
|||
|
|||
// We optimize for the case where index is not realized to return null as quickly as we can.
|
|||
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
|
|||
// If an index that is realized is requested by the layout, we unfortunately have to walk the
|
|||
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
|
|||
// and non-virtualizing hosts.
|
|||
private IControl GetElementIfAlreadyHeldByLayout(int index) |
|||
{ |
|||
IControl element = null; |
|||
|
|||
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; |
|||
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); |
|||
|
|||
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) |
|||
{ |
|||
foreach (var child in _owner.Children) |
|||
{ |
|||
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); |
|||
if (virtInfo?.IsHeldByLayout == true) |
|||
{ |
|||
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
|
|||
int childIndex = virtInfo.Index; |
|||
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex); |
|||
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex); |
|||
if (virtInfo.Index == index) |
|||
{ |
|||
element = child; |
|||
// If we have valid first/last indices, we don't have to walk the rest, but if we
|
|||
// do not, then we keep walking through the entire children collection to get accurate
|
|||
// indices once.
|
|||
if (!cachedFirstLastIndicesInvalid) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
private IControl GetElementFromUniqueIdResetPool(int index) |
|||
{ |
|||
IControl element = null; |
|||
// See if you can get it from the reset pool.
|
|||
if (_isDataSourceStableResetPending) |
|||
{ |
|||
element = _resetPool.Remove(index); |
|||
if (element != null) |
|||
{ |
|||
// Make sure that the index is updated to the current one
|
|||
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); |
|||
UpdateElementIndex(element, virtInfo, index); |
|||
} |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
private IControl GetElementFromPinnedElements(int index) |
|||
{ |
|||
IControl element = null; |
|||
|
|||
// See if you can find something among the pinned elements.
|
|||
for (var i = 0; i < _pinnedPool.Count; ++i) |
|||
{ |
|||
var elementInfo = _pinnedPool[i]; |
|||
var virtInfo = elementInfo.VirtualizationInfo; |
|||
|
|||
if (virtInfo.Index == index) |
|||
{ |
|||
_pinnedPool.RemoveAt(i); |
|||
element = elementInfo.PinnedElement; |
|||
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return element; |
|||
} |
|||
|
|||
private IControl GetElementFromElementFactory(int index) |
|||
{ |
|||
// The view generator is the provider of last resort.
|
|||
|
|||
var itemTemplateFactory = _owner.ItemTemplateShim; |
|||
if (itemTemplateFactory == null) |
|||
{ |
|||
// If no ItemTemplate was provided, use a default
|
|||
var factory = FuncDataTemplate.Default; |
|||
_owner.ItemTemplate = factory; |
|||
itemTemplateFactory = _owner.ItemTemplateShim; |
|||
} |
|||
|
|||
var data = _owner.ItemsSourceView.GetAt(index); |
|||
var element = itemTemplateFactory.GetElement(_owner, data); |
|||
|
|||
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); |
|||
if (virtInfo == null) |
|||
{ |
|||
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); |
|||
} |
|||
|
|||
// Prepare the element
|
|||
element.DataContext = data; |
|||
|
|||
virtInfo.MoveOwnershipToLayoutFromElementFactory( |
|||
index, |
|||
/* uniqueId: */ |
|||
_owner.ItemsSourceView.HasKeyIndexMapping ? |
|||
_owner.ItemsSourceView.KeyFromIndex(index) : |
|||
string.Empty); |
|||
|
|||
// The view generator is the only provider that prepares the element.
|
|||
var repeater = _owner; |
|||
|
|||
// Add the element to the children collection here before raising OnElementPrepared so
|
|||
// that handlers can walk up the tree in case they want to find their IndexPath in the
|
|||
// nested case.
|
|||
var children = repeater.Children; |
|||
if (element.VisualParent != repeater) |
|||
{ |
|||
children.Add(element); |
|||
} |
|||
|
|||
repeater.OnElementPrepared(element, index); |
|||
|
|||
// Update realized indices
|
|||
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); |
|||
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); |
|||
|
|||
return element; |
|||
} |
|||
|
|||
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) |
|||
{ |
|||
if (_isDataSourceStableResetPending) |
|||
{ |
|||
_resetPool.Add(element); |
|||
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); |
|||
} |
|||
|
|||
return _isDataSourceStableResetPending; |
|||
} |
|||
|
|||
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) |
|||
{ |
|||
if (_isDataSourceStableResetPending) |
|||
{ |
|||
_resetPool.Add(element); |
|||
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); |
|||
} |
|||
|
|||
return _isDataSourceStableResetPending; |
|||
} |
|||
|
|||
private void UpdateFocusedElement() |
|||
{ |
|||
IControl focusedElement = null; |
|||
|
|||
var child = FocusManager.Instance.Current; |
|||
|
|||
if (child != null) |
|||
{ |
|||
var parent = child.VisualParent; |
|||
var owner = _owner; |
|||
|
|||
// Find out if the focused element belongs to one of our direct
|
|||
// children.
|
|||
while (parent != null) |
|||
{ |
|||
if (parent is ItemsRepeater repeater) |
|||
{ |
|||
var element = child as IControl; |
|||
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) |
|||
{ |
|||
focusedElement = element; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
child = parent as IInputElement; |
|||
parent = child.VisualParent; |
|||
} |
|||
} |
|||
|
|||
// If the focused element has changed,
|
|||
// we need to unpin the old one and pin the new one.
|
|||
if (_lastFocusedElement != focusedElement) |
|||
{ |
|||
if (_lastFocusedElement != null) |
|||
{ |
|||
UpdatePin(_lastFocusedElement, false /* addPin */); |
|||
} |
|||
|
|||
if (focusedElement != null) |
|||
{ |
|||
UpdatePin(focusedElement, true /* addPin */); |
|||
} |
|||
|
|||
_lastFocusedElement = focusedElement; |
|||
} |
|||
} |
|||
|
|||
private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement(); |
|||
|
|||
private void EnsureEventSubscriptions() |
|||
{ |
|||
if (!_eventsSubscribed) |
|||
{ |
|||
_owner.GotFocus += OnFocusChanged; |
|||
_owner.LostFocus += OnFocusChanged; |
|||
} |
|||
} |
|||
|
|||
private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index) |
|||
{ |
|||
var oldIndex = virtInfo.Index; |
|||
if (oldIndex != index) |
|||
{ |
|||
virtInfo.UpdateIndex(index); |
|||
_owner.OnElementIndexChanged(element, oldIndex, index); |
|||
} |
|||
} |
|||
|
|||
private void InvalidateRealizedIndicesHeldByLayout() |
|||
{ |
|||
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; |
|||
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; |
|||
} |
|||
|
|||
private struct PinnedElementInfo |
|||
{ |
|||
public PinnedElementInfo(IControl element) |
|||
{ |
|||
PinnedElement = element; |
|||
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); |
|||
} |
|||
|
|||
public IControl PinnedElement { get; } |
|||
public VirtualizationInfo VirtualizationInfo { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,501 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class ViewportManager |
|||
{ |
|||
private const double CacheBufferPerSideInflationPixelDelta = 40.0; |
|||
private readonly ItemsRepeater _owner; |
|||
private bool _ensuredScroller; |
|||
private IScrollAnchorProvider _scroller; |
|||
private IControl _makeAnchorElement; |
|||
private bool _isAnchorOutsideRealizedRange; |
|||
private Task _cacheBuildAction; |
|||
private Rect _visibleWindow; |
|||
private Rect _layoutExtent; |
|||
// This is the expected shift by the layout.
|
|||
private Point _expectedViewportShift; |
|||
// This is what is pending and not been accounted for.
|
|||
// Sometimes the scrolling surface cannot service a shift (for example
|
|||
// it is already at the top and cannot shift anymore.)
|
|||
private Point _pendingViewportShift; |
|||
// Unshiftable shift amount that this view manager can
|
|||
// handle on its own to fake it to the layout as if the shift
|
|||
// actually happened. This can happen in cases where no scrollviewer
|
|||
// in the parent chain can scroll in the shift direction.
|
|||
private Point _unshiftableShift; |
|||
private double _maximumHorizontalCacheLength = 0.0; |
|||
private double _maximumVerticalCacheLength = 0.0; |
|||
private double _horizontalCacheBufferPerSide; |
|||
private double _verticalCacheBufferPerSide; |
|||
private bool _isBringIntoViewInProgress; |
|||
// For non-virtualizing layouts, we do not need to keep
|
|||
// updating viewports and invalidating measure often. So when
|
|||
// a non virtualizing layout is used, we stop doing all that work.
|
|||
bool _managingViewportDisabled; |
|||
private IDisposable _effectiveViewportChangedRevoker; |
|||
private bool _layoutUpdatedSubscribed; |
|||
|
|||
public ViewportManager(ItemsRepeater owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
public IControl SuggestedAnchor |
|||
{ |
|||
get |
|||
{ |
|||
// The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
|
|||
var suggestedAnchor = _makeAnchorElement; |
|||
var owner = _owner; |
|||
|
|||
if (suggestedAnchor == null) |
|||
{ |
|||
var anchorElement = _scroller?.CurrentAnchor; |
|||
|
|||
if (anchorElement != null) |
|||
{ |
|||
// We can't simply return anchorElement because, in case of nested Repeaters, it may not
|
|||
// be a direct child of ours, or even an indirect child. We need to walk up the tree starting
|
|||
// from anchorElement to figure out what child of ours (if any) to use as the suggested element.
|
|||
var child = anchorElement; |
|||
var parent = child.VisualParent as IControl; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
if (parent == owner) |
|||
{ |
|||
suggestedAnchor = child; |
|||
break; |
|||
} |
|||
|
|||
child = parent; |
|||
parent = parent.VisualParent as IControl; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return suggestedAnchor; |
|||
} |
|||
} |
|||
|
|||
public bool HasScroller => _scroller != null; |
|||
|
|||
public IControl MadeAnchor => _makeAnchorElement; |
|||
|
|||
public double HorizontalCacheLength |
|||
{ |
|||
get => _maximumHorizontalCacheLength; |
|||
set |
|||
{ |
|||
if (_maximumHorizontalCacheLength != value) |
|||
{ |
|||
ValidateCacheLength(value); |
|||
_maximumHorizontalCacheLength = value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public double VerticalCacheLength |
|||
{ |
|||
get => _maximumVerticalCacheLength; |
|||
set |
|||
{ |
|||
if (_maximumVerticalCacheLength != value) |
|||
{ |
|||
ValidateCacheLength(value); |
|||
_maximumVerticalCacheLength = value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Rect GetLayoutVisibleWindowDiscardAnchor() |
|||
{ |
|||
var visibleWindow = _visibleWindow; |
|||
|
|||
if (HasScroller) |
|||
{ |
|||
visibleWindow = new Rect( |
|||
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, |
|||
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, |
|||
visibleWindow.Width, |
|||
visibleWindow.Height); |
|||
} |
|||
|
|||
return visibleWindow; |
|||
} |
|||
|
|||
public Rect GetLayoutVisibleWindow() |
|||
{ |
|||
var visibleWindow = _visibleWindow; |
|||
|
|||
if (_makeAnchorElement != null) |
|||
{ |
|||
// The anchor is not necessarily laid out yet. Its position should default
|
|||
// to zero and the layout origin is expected to change once layout is done.
|
|||
// Until then, we need a window that's going to protect the anchor from
|
|||
// getting recycled.
|
|||
visibleWindow = visibleWindow.WithX(0).WithY(0); |
|||
} |
|||
else if (HasScroller) |
|||
{ |
|||
visibleWindow = new Rect( |
|||
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, |
|||
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, |
|||
visibleWindow.Width, |
|||
visibleWindow.Height); |
|||
} |
|||
|
|||
return visibleWindow; |
|||
} |
|||
|
|||
public Rect GetLayoutRealizationWindow() |
|||
{ |
|||
var realizationWindow = GetLayoutVisibleWindow(); |
|||
if (HasScroller) |
|||
{ |
|||
realizationWindow = new Rect( |
|||
realizationWindow.X - _horizontalCacheBufferPerSide, |
|||
realizationWindow.Y - _verticalCacheBufferPerSide, |
|||
realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0, |
|||
realizationWindow.Height + _verticalCacheBufferPerSide * 2.0); |
|||
} |
|||
|
|||
return realizationWindow; |
|||
} |
|||
|
|||
public void SetLayoutExtent(Rect extent) |
|||
{ |
|||
_expectedViewportShift = new Point( |
|||
_expectedViewportShift.X + _layoutExtent.X - extent.X, |
|||
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y); |
|||
|
|||
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
|
|||
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) |
|||
{ |
|||
// There are cases where we might be expecting a shift but not get it. We will
|
|||
// be waiting for the effective viewport event but if the scroll viewer is not able
|
|||
// to perform the shift (perhaps because it cannot scroll in negative offset),
|
|||
// then we will end up not realizing elements in the visible
|
|||
// window. To avoid this, we register to layout updated for this layout pass. If we
|
|||
// get an effective viewport, we know we have a new viewport and we unregister from
|
|||
// layout updated. If we get the layout updated handler, then we know that the
|
|||
// scroller was unable to perform the shift and we invalidate measure and unregister
|
|||
// from the layout updated event.
|
|||
if (!_layoutUpdatedSubscribed) |
|||
{ |
|||
_owner.LayoutUpdated += OnLayoutUpdated; |
|||
_layoutUpdatedSubscribed = true; |
|||
} |
|||
} |
|||
|
|||
_layoutExtent = extent; |
|||
_pendingViewportShift = _expectedViewportShift; |
|||
|
|||
// We just finished a measure pass and have a new extent.
|
|||
// Let's make sure the scrollers will run its arrange so that they track the anchor.
|
|||
((IControl)_scroller)?.InvalidateArrange(); |
|||
} |
|||
|
|||
public Point GetOrigin() => _layoutExtent.TopLeft; |
|||
|
|||
public void OnLayoutChanged(bool isVirtualizing) |
|||
{ |
|||
_managingViewportDisabled = !isVirtualizing; |
|||
|
|||
_layoutExtent = default; |
|||
_expectedViewportShift = default; |
|||
_pendingViewportShift = default; |
|||
_unshiftableShift = default; |
|||
|
|||
_effectiveViewportChangedRevoker?.Dispose(); |
|||
|
|||
if (!_managingViewportDisabled) |
|||
{ |
|||
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); |
|||
} |
|||
} |
|||
|
|||
public void OnElementPrepared(IControl element) |
|||
{ |
|||
// If we have an anchor element, we do not want the
|
|||
// scroll anchor provider to start anchoring some other element.
|
|||
////element.CanBeScrollAnchor(true);
|
|||
} |
|||
|
|||
public void OnElementCleared(ILayoutable element) |
|||
{ |
|||
////element.CanBeScrollAnchor(false);
|
|||
} |
|||
|
|||
public void OnOwnerMeasuring() |
|||
{ |
|||
// This is because of a bug that causes effective viewport to not
|
|||
// fire if you register during arrange.
|
|||
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
|
|||
EnsureScroller(); |
|||
} |
|||
|
|||
public void OnOwnerArranged() |
|||
{ |
|||
_expectedViewportShift = default; |
|||
|
|||
if (!_managingViewportDisabled) |
|||
{ |
|||
// This is because of a bug that causes effective viewport to not
|
|||
// fire if you register during arrange.
|
|||
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
|
|||
// EnsureScroller();
|
|||
|
|||
if (HasScroller) |
|||
{ |
|||
double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0; |
|||
double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0; |
|||
|
|||
bool continueBuildingCache = |
|||
_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || |
|||
_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; |
|||
|
|||
if (continueBuildingCache) |
|||
{ |
|||
_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; |
|||
_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; |
|||
|
|||
_horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); |
|||
_verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void OnLayoutUpdated(object sender, EventArgs args) |
|||
{ |
|||
_owner.LayoutUpdated -= OnLayoutUpdated; |
|||
if (_managingViewportDisabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// We were expecting a viewport shift but we never got one and we are not going to in this
|
|||
// layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
|
|||
// adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
|
|||
// that can scroll in the direction where the shift is expected.
|
|||
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) |
|||
{ |
|||
// Assume this is never going to come.
|
|||
_unshiftableShift = new Point( |
|||
_unshiftableShift.X + _pendingViewportShift.X, |
|||
_unshiftableShift.Y + _pendingViewportShift.Y); |
|||
_pendingViewportShift = default; |
|||
_expectedViewportShift = default; |
|||
|
|||
TryInvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) |
|||
{ |
|||
_makeAnchorElement = anchor; |
|||
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; |
|||
} |
|||
|
|||
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) |
|||
{ |
|||
if (!_managingViewportDisabled) |
|||
{ |
|||
// During the time between a bring into view request and the element coming into view we do not
|
|||
// want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
|
|||
// element that is being brought into view. We can do this by making just that element as a potential
|
|||
// anchor candidate and ensure no other element of this repeater is an anchor candidate.
|
|||
// Once the layout pass is done and we render the frame, the element will be in frame and we can
|
|||
// switch back to letting the anchor provider pick a suitable anchor.
|
|||
|
|||
// get the targetChild - i.e the immediate child of this repeater that is being brought into view.
|
|||
// Note that the element being brought into view could be a descendant.
|
|||
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); |
|||
|
|||
// Make sure that only the target child can be the anchor during the bring into view operation.
|
|||
foreach (var child in _owner.Children) |
|||
{ |
|||
////if (child.CanBeScrollAnchor && child != targetChild)
|
|||
////{
|
|||
//// child.CanBeScrollAnchor = false;
|
|||
////}
|
|||
} |
|||
|
|||
// Register to rendering event to go back to how things were before where any child can be the anchor.
|
|||
_isBringIntoViewInProgress = true; |
|||
////if (!m_renderingToken)
|
|||
////{
|
|||
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
|
|||
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
|
|||
////}
|
|||
} |
|||
} |
|||
|
|||
private IControl GetImmediateChildOfRepeater(IControl descendant) |
|||
{ |
|||
var targetChild = descendant; |
|||
var parent = descendant.Parent; |
|||
while (parent != null && parent != _owner) |
|||
{ |
|||
targetChild = parent; |
|||
parent = (IControl)parent.VisualParent; |
|||
} |
|||
|
|||
if (parent == null) |
|||
{ |
|||
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); |
|||
} |
|||
|
|||
return targetChild; |
|||
} |
|||
|
|||
public void ResetScrollers() |
|||
{ |
|||
_scroller = null; |
|||
_effectiveViewportChangedRevoker?.Dispose(); |
|||
_effectiveViewportChangedRevoker = null; |
|||
_ensuredScroller = false; |
|||
} |
|||
|
|||
private void OnEffectiveViewportChanged(TransformedBounds? bounds) |
|||
{ |
|||
if (!bounds.HasValue) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var globalClip = bounds.Value.Clip; |
|||
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; |
|||
var clip = globalClip.TransformToAABB(transform); |
|||
var effectiveViewport = clip.Intersect(bounds.Value.Bounds); |
|||
|
|||
UpdateViewport(effectiveViewport); |
|||
|
|||
_pendingViewportShift = default; |
|||
_unshiftableShift = default; |
|||
if (_visibleWindow.IsEmpty) |
|||
{ |
|||
// We got cleared.
|
|||
_layoutExtent = default; |
|||
} |
|||
|
|||
// We got a new viewport, we dont need to wait for layout updated anymore to
|
|||
// see if our request for a pending shift was handled.
|
|||
if (_layoutUpdatedSubscribed) |
|||
{ |
|||
_owner.LayoutUpdated -= OnLayoutUpdated; |
|||
} |
|||
} |
|||
|
|||
private void EnsureScroller() |
|||
{ |
|||
if (!_ensuredScroller) |
|||
{ |
|||
ResetScrollers(); |
|||
|
|||
var parent = _owner.GetVisualParent(); |
|||
while (parent != null) |
|||
{ |
|||
if (parent is IScrollAnchorProvider scroller) |
|||
{ |
|||
_scroller = scroller; |
|||
break; |
|||
} |
|||
|
|||
parent = parent.VisualParent; |
|||
} |
|||
|
|||
if (_scroller == null) |
|||
{ |
|||
// We usually update the viewport in the post arrange handler. But, since we don't have
|
|||
// a scroller, let's do it now.
|
|||
UpdateViewport(Rect.Empty); |
|||
} |
|||
else if (!_managingViewportDisabled) |
|||
{ |
|||
_effectiveViewportChangedRevoker?.Dispose(); |
|||
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); |
|||
} |
|||
|
|||
_ensuredScroller = true; |
|||
} |
|||
} |
|||
|
|||
private void UpdateViewport(Rect viewport) |
|||
{ |
|||
var currentVisibleWindow = viewport; |
|||
|
|||
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && |
|||
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) |
|||
{ |
|||
// We got cleared.
|
|||
_visibleWindow = default; |
|||
} |
|||
else |
|||
{ |
|||
_visibleWindow = currentVisibleWindow; |
|||
} |
|||
|
|||
TryInvalidateMeasure(); |
|||
} |
|||
|
|||
private static void ValidateCacheLength(double cacheLength) |
|||
{ |
|||
if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) |
|||
{ |
|||
throw new ArgumentException("The maximum cache length must be equal or superior to zero."); |
|||
} |
|||
} |
|||
|
|||
private void TryInvalidateMeasure() |
|||
{ |
|||
// Don't invalidate measure if we have an invalid window.
|
|||
if (!_visibleWindow.IsEmpty) |
|||
{ |
|||
// We invalidate measure instead of just invalidating arrange because
|
|||
// we don't invalidate measure in UpdateViewport if the view is changing to
|
|||
// avoid layout cycles.
|
|||
_owner.InvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
private IDisposable SubscribeToEffectiveViewportChanged(IControl control) |
|||
{ |
|||
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
|
|||
// we can get this from TransformedBounds, but this property is updated after layout has
|
|||
// run, resulting in the UI being updated too late when scrolling quickly. We can
|
|||
// partially remedey this by triggering also on Bounds changes, but this won't work so
|
|||
// well for nested ItemsRepeaters.
|
|||
//
|
|||
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
|
|||
// for this case): we need to implement that in Avalonia.
|
|||
return control.GetObservable(Visual.TransformedBoundsProperty) |
|||
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) |
|||
.Skip(1) |
|||
.Subscribe(OnEffectiveViewportChanged); |
|||
} |
|||
|
|||
private class ScrollerInfo |
|||
{ |
|||
public ScrollerInfo(ScrollViewer scroller) |
|||
{ |
|||
Scroller = scroller; |
|||
} |
|||
|
|||
public ScrollViewer Scroller { get; } |
|||
} |
|||
}; |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// 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.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal enum ElementOwner |
|||
{ |
|||
// All elements are originally owned by the view generator.
|
|||
ElementFactory, |
|||
// Ownership is transferred to the layout when it calls GetElement.
|
|||
Layout, |
|||
// Ownership is transferred to the pinned pool if the element is cleared (outside of
|
|||
// a 'remove' collection change of course).
|
|||
PinnedPool, |
|||
// Ownership is transfered to the reset pool if the element is cleared by a reset and
|
|||
// the data source supports unique ids.
|
|||
UniqueIdResetPool, |
|||
// Ownership is transfered to the animator if the element is cleared due to a
|
|||
// 'remove'-like collection change.
|
|||
Animator |
|||
} |
|||
|
|||
internal class VirtualizationInfo |
|||
{ |
|||
private int _pinCounter; |
|||
private object _data; |
|||
|
|||
public Rect ArrangeBounds { get; set; } |
|||
public bool AutoRecycleCandidate { get; set; } |
|||
public int Index { get; private set; } |
|||
public bool IsPinned => _pinCounter > 0; |
|||
public bool IsHeldByLayout => Owner == ElementOwner.Layout; |
|||
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; |
|||
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; |
|||
public bool KeepAlive { get; set; } |
|||
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; |
|||
public string UniqueId { get; private set; } |
|||
|
|||
public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) |
|||
{ |
|||
Owner = ElementOwner.Layout; |
|||
Index = index; |
|||
UniqueId = uniqueId; |
|||
} |
|||
|
|||
public void MoveOwnershipToLayoutFromUniqueIdResetPool() |
|||
{ |
|||
Owner = ElementOwner.Layout; |
|||
} |
|||
|
|||
public void MoveOwnershipToLayoutFromPinnedPool() |
|||
{ |
|||
Owner = ElementOwner.Layout; |
|||
} |
|||
|
|||
public void MoveOwnershipToElementFactory() |
|||
{ |
|||
Owner = ElementOwner.ElementFactory; |
|||
_pinCounter = 0; |
|||
Index = -1; |
|||
UniqueId = string.Empty; |
|||
ArrangeBounds = ItemsRepeater.InvalidRect; |
|||
} |
|||
|
|||
public void MoveOwnershipToUniqueIdResetPoolFromLayout() |
|||
{ |
|||
Owner = ElementOwner.UniqueIdResetPool; |
|||
// Keep the pinCounter the same. If the container survives the reset
|
|||
// it can go on being pinned as if nothing happened.
|
|||
} |
|||
|
|||
public void MoveOwnershipToAnimator() |
|||
{ |
|||
// During a unique id reset, some elements might get removed.
|
|||
// Their ownership will go from the UniqueIdResetPool to the Animator.
|
|||
// The common path though is for ownership to go from Layout to Animator.
|
|||
Owner = ElementOwner.Animator; |
|||
Index = -1; |
|||
_pinCounter = 0; |
|||
} |
|||
|
|||
public void MoveOwnershipToPinnedPool() |
|||
{ |
|||
Owner = ElementOwner.PinnedPool; |
|||
} |
|||
|
|||
public int AddPin() |
|||
{ |
|||
if (!IsRealized) |
|||
{ |
|||
throw new InvalidOperationException("You can't pin an unrealized element."); |
|||
} |
|||
|
|||
return ++_pinCounter; |
|||
} |
|||
|
|||
public int RemovePin() |
|||
{ |
|||
if (!IsRealized) |
|||
{ |
|||
throw new InvalidOperationException("You can't unpin an unrealized element."); |
|||
} |
|||
|
|||
if (!IsPinned) |
|||
{ |
|||
throw new InvalidOperationException("UnpinElement was called more often than PinElement."); |
|||
} |
|||
|
|||
return --_pinCounter; |
|||
} |
|||
|
|||
public void UpdateIndex(int newIndex) => Index = newIndex; |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Controls.Templates |
|||
{ |
|||
/// <summary>
|
|||
/// Selects a member of an object using a <see cref="Func{TObject, TMember}"/>.
|
|||
/// </summary>
|
|||
public class FuncMemberSelector<TObject, TMember> : IMemberSelector |
|||
{ |
|||
private readonly Func<TObject, TMember> _selector; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FuncMemberSelector{TObject, TMember}"/>
|
|||
/// class.
|
|||
/// </summary>
|
|||
/// <param name="selector">The selector.</param>
|
|||
public FuncMemberSelector(Func<TObject, TMember> selector) |
|||
{ |
|||
this._selector = selector; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Selects a member of an object.
|
|||
/// </summary>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <returns>The selected member.</returns>
|
|||
public object Select(object o) |
|||
{ |
|||
return (o is TObject) ? _selector((TObject)o) : default(TMember); |
|||
} |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Controls.Templates |
|||
{ |
|||
/// <summary>
|
|||
/// Selects a member of an object.
|
|||
/// </summary>
|
|||
public interface IMemberSelector |
|||
{ |
|||
/// <summary>
|
|||
/// Selects a member of an object.
|
|||
/// </summary>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <returns>The selected member.</returns>
|
|||
object Select(object o); |
|||
} |
|||
} |
|||
@ -1,23 +1,24 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Avalonia.Diagnostics.DevTools"> |
|||
<Grid RowDefinitions="Auto,*,Auto"> |
|||
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}"> |
|||
<TabStripItem Content="Logical Tree"/> |
|||
<TabStripItem Content="Visual Tree"/> |
|||
<TabStripItem Content="Events"/> |
|||
</TabStrip> |
|||
<Grid RowDefinitions="*,Auto" Margin="4"> |
|||
|
|||
<ContentControl Content="{Binding Content}" Grid.Row="1"/> |
|||
|
|||
<StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2"> |
|||
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}"> |
|||
<TabControl.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock Text="{Binding Name}" /> |
|||
</DataTemplate> |
|||
</TabControl.ItemTemplate> |
|||
</TabControl> |
|||
|
|||
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal"> |
|||
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock> |
|||
<Separator Width="8"/> |
|||
<Separator Width="8" /> |
|||
<TextBlock>Focused:</TextBlock> |
|||
<TextBlock Text="{Binding FocusedControl}"/> |
|||
<Separator Width="8"/> |
|||
<TextBlock Text="{Binding FocusedControl}" /> |
|||
<Separator Width="8" /> |
|||
<TextBlock>Pointer Over:</TextBlock> |
|||
<TextBlock Text="{Binding PointerOverElement}"/> |
|||
<TextBlock Text="{Binding PointerOverElement}" /> |
|||
</StackPanel> |
|||
</Grid> |
|||
</UserControl> |
|||
|
|||
@ -0,0 +1,16 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
/// <summary>
|
|||
/// View model interface for tool showing up in DevTools
|
|||
/// </summary>
|
|||
public interface IDevToolViewModel |
|||
{ |
|||
/// <summary>
|
|||
/// Name of a tool.
|
|||
/// </summary>
|
|||
string Name { get; } |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue