Browse Source
# Conflicts: # src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cspull/4529/head
103 changed files with 7515 additions and 6945 deletions
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="2.80.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.0" /> |
|||
<PackageReference Include="SkiaSharp" Version="2.80.2-preview.33" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2-preview.33" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -1,41 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Reflection; |
|||
using System.Windows.Input; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Data.Converters |
|||
{ |
|||
class AlwaysEnabledDelegateCommand : ICommand |
|||
{ |
|||
private readonly Delegate action; |
|||
|
|||
private ParameterInfo parameterInfo; |
|||
|
|||
public AlwaysEnabledDelegateCommand(Delegate action) |
|||
{ |
|||
this.action = action; |
|||
var parameters = action.Method.GetParameters(); |
|||
parameterInfo = parameters.Length == 0 ? null : parameters[0]; |
|||
} |
|||
|
|||
#pragma warning disable 0067
|
|||
public event EventHandler CanExecuteChanged; |
|||
#pragma warning restore 0067
|
|||
|
|||
public bool CanExecute(object parameter) => true; |
|||
|
|||
public void Execute(object parameter) |
|||
{ |
|||
if (parameterInfo == null) |
|||
{ |
|||
action.DynamicInvoke(); |
|||
} |
|||
else |
|||
{ |
|||
TypeUtilities.TryConvert(parameterInfo.ParameterType, parameter, CultureInfo.CurrentCulture, out object convertedParameter); |
|||
action.DynamicInvoke(convertedParameter); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,230 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using System.Windows.Input; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Data.Converters |
|||
{ |
|||
class MethodToCommandConverter : ICommand |
|||
{ |
|||
readonly static Func<object, bool> AlwaysEnabled = (_) => true; |
|||
readonly static MethodInfo tryConvert = typeof(TypeUtilities) |
|||
.GetMethod(nameof(TypeUtilities.TryConvert), BindingFlags.Public | BindingFlags.Static); |
|||
readonly static PropertyInfo currentCulture = typeof(CultureInfo) |
|||
.GetProperty(nameof(CultureInfo.CurrentCulture), BindingFlags.Public | BindingFlags.Static); |
|||
readonly Func<object, bool> canExecute; |
|||
readonly Action<object> execute; |
|||
readonly WeakPropertyChangedProxy weakPropertyChanged; |
|||
readonly PropertyChangedEventHandler propertyChangedEventHandler; |
|||
readonly string[] dependencyProperties; |
|||
|
|||
public MethodToCommandConverter(Delegate action) |
|||
{ |
|||
var target = action.Target; |
|||
var canExecuteMethodName = "Can" + action.Method.Name; |
|||
var parameters = action.Method.GetParameters(); |
|||
var parameterInfo = parameters.Length == 0 ? null : parameters[0].ParameterType; |
|||
|
|||
if (parameterInfo == null) |
|||
{ |
|||
execute = CreateExecute(target, action.Method); |
|||
} |
|||
else |
|||
{ |
|||
execute = CreateExecute(target, action.Method, parameterInfo); |
|||
} |
|||
|
|||
var canExecuteMethod = action.Method.DeclaringType.GetRuntimeMethods() |
|||
.FirstOrDefault(m => m.Name == canExecuteMethodName |
|||
&& m.GetParameters().Length == 1 |
|||
&& m.GetParameters()[0].ParameterType == typeof(object)); |
|||
if (canExecuteMethod == null) |
|||
{ |
|||
canExecute = AlwaysEnabled; |
|||
} |
|||
else |
|||
{ |
|||
canExecute = CreateCanExecute(target, canExecuteMethod); |
|||
dependencyProperties = canExecuteMethod |
|||
.GetCustomAttributes(typeof(Metadata.DependsOnAttribute), true) |
|||
.OfType<Metadata.DependsOnAttribute>() |
|||
.Select(x => x.Name) |
|||
.ToArray(); |
|||
if (dependencyProperties.Any() |
|||
&& target is INotifyPropertyChanged inpc) |
|||
{ |
|||
propertyChangedEventHandler = OnPropertyChanged; |
|||
weakPropertyChanged = new WeakPropertyChangedProxy(inpc, propertyChangedEventHandler); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void OnPropertyChanged(object sender,PropertyChangedEventArgs args) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(args.PropertyName) |
|||
|| dependencyProperties?.Contains(args.PropertyName) == true) |
|||
{ |
|||
Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty) |
|||
, Threading.DispatcherPriority.Input); |
|||
} |
|||
} |
|||
|
|||
#pragma warning disable 0067
|
|||
public event EventHandler CanExecuteChanged; |
|||
#pragma warning restore 0067
|
|||
|
|||
public bool CanExecute(object parameter) => canExecute(parameter); |
|||
|
|||
public void Execute(object parameter) => execute(parameter); |
|||
|
|||
|
|||
static Action<object> CreateExecute(object target |
|||
, System.Reflection.MethodInfo method) |
|||
{ |
|||
|
|||
var parameter = Expression.Parameter(typeof(object), "parameter"); |
|||
|
|||
var instance = Expression.Convert |
|||
( |
|||
Expression.Constant(target), |
|||
method.DeclaringType |
|||
); |
|||
|
|||
|
|||
var call = Expression.Call |
|||
( |
|||
instance, |
|||
method |
|||
); |
|||
|
|||
|
|||
return Expression |
|||
.Lambda<Action<object>>(call, parameter) |
|||
.Compile(); |
|||
} |
|||
|
|||
static Action<object> CreateExecute(object target |
|||
, System.Reflection.MethodInfo method |
|||
, Type parameterType) |
|||
{ |
|||
|
|||
var parameter = Expression.Parameter(typeof(object), "parameter"); |
|||
|
|||
var instance = Expression.Convert |
|||
( |
|||
Expression.Constant(target), |
|||
method.DeclaringType |
|||
); |
|||
|
|||
Expression body; |
|||
|
|||
if (parameterType == typeof(object)) |
|||
{ |
|||
body = Expression.Call(instance, |
|||
method, |
|||
parameter |
|||
); |
|||
} |
|||
else |
|||
{ |
|||
var arg0 = Expression.Variable(typeof(object), "argX"); |
|||
var convertCall = Expression.Call(tryConvert, |
|||
Expression.Constant(parameterType), |
|||
parameter, |
|||
Expression.Property(null, currentCulture), |
|||
arg0 |
|||
); |
|||
|
|||
var call = Expression.Call(instance, |
|||
method, |
|||
Expression.Convert(arg0, parameterType) |
|||
); |
|||
body = Expression.Block(new[] { arg0 }, |
|||
convertCall, |
|||
call |
|||
); |
|||
|
|||
} |
|||
Action<object> action = null; |
|||
try |
|||
{ |
|||
action = Expression |
|||
.Lambda<Action<object>>(body, parameter) |
|||
.Compile(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
throw ex; |
|||
} |
|||
return action; |
|||
} |
|||
|
|||
static Func<object, bool> CreateCanExecute(object target |
|||
, System.Reflection.MethodInfo method) |
|||
{ |
|||
var parameter = Expression.Parameter(typeof(object), "parameter"); |
|||
var instance = Expression.Convert |
|||
( |
|||
Expression.Constant(target), |
|||
method.DeclaringType |
|||
); |
|||
var call = Expression.Call |
|||
( |
|||
instance, |
|||
method, |
|||
parameter |
|||
); |
|||
return Expression |
|||
.Lambda<Func<object, bool>>(call, parameter) |
|||
.Compile(); |
|||
} |
|||
|
|||
|
|||
internal class WeakPropertyChangedProxy |
|||
{ |
|||
readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null); |
|||
readonly PropertyChangedEventHandler _handler; |
|||
internal WeakReference<INotifyPropertyChanged> Source { get; } = new WeakReference<INotifyPropertyChanged>(null); |
|||
|
|||
public WeakPropertyChangedProxy() |
|||
{ |
|||
_handler = new PropertyChangedEventHandler(OnPropertyChanged); |
|||
} |
|||
|
|||
public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this() |
|||
{ |
|||
SubscribeTo(source, listener); |
|||
} |
|||
|
|||
public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener) |
|||
{ |
|||
source.PropertyChanged += _handler; |
|||
|
|||
Source.SetTarget(source); |
|||
_listener.SetTarget(listener); |
|||
} |
|||
|
|||
public void Unsubscribe() |
|||
{ |
|||
if (Source.TryGetTarget(out INotifyPropertyChanged source) && source != null) |
|||
source.PropertyChanged -= _handler; |
|||
|
|||
Source.SetTarget(null); |
|||
_listener.SetTarget(null); |
|||
} |
|||
|
|||
void OnPropertyChanged(object sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (_listener.TryGetTarget(out var handler) && handler != null) |
|||
handler(sender, e); |
|||
else |
|||
Unsubscribe(); |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,645 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<Styles.Resources> |
|||
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">12,0,12,0</Thickness> |
|||
|
|||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double> |
|||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double> |
|||
|
|||
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry> |
|||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry> |
|||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry> |
|||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry> |
|||
|
|||
<SolidColorBrush x:Key="DataGridGridLinesBrush" |
|||
Color="{StaticResource SystemBaseMediumLowColor}" |
|||
Opacity="0.4" /> |
|||
<SolidColorBrush x:Key="DataGridDropLocationIndicatorBackground" |
|||
Color="#3F4346" /> |
|||
<SolidColorBrush x:Key="DataGridDisabledVisualElementBackground" |
|||
Color="#8CFFFFFF" /> |
|||
<SolidColorBrush x:Key="DataGridFillerGridLinesBrush" |
|||
Color="Transparent" /> |
|||
<SolidColorBrush x:Key="DataGridCurrencyVisualPrimaryBrush" |
|||
Color="Transparent" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderBackgroundColor" |
|||
ResourceKey="SystemAltHighColor" /> |
|||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" |
|||
Color="{StaticResource DataGridColumnHeaderBackgroundColor}" /> |
|||
<StaticResource x:Key="DataGridScrollBarsSeparatorBackground" |
|||
ResourceKey="SystemChromeLowColor" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderForegroundBrush" |
|||
ResourceKey="SystemControlForegroundBaseMediumBrush" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderHoveredBackgroundColor" |
|||
ResourceKey="SystemListLowColor" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderPressedBackgroundColor" |
|||
ResourceKey="SystemListMediumColor" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderDraggedBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundChromeMediumLowBrush" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderPointerOverBrush" |
|||
ResourceKey="SystemControlHighlightListLowBrush" /> |
|||
<StaticResource x:Key="DataGridColumnHeaderPressedBrush" |
|||
ResourceKey="SystemControlHighlightListMediumBrush" /> |
|||
<StaticResource x:Key="DataGridDetailsPresenterBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundChromeMediumLowBrush" /> |
|||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" |
|||
ResourceKey="DataGridFillerGridLinesBrush" /> |
|||
<StaticResource x:Key="DataGridRowSelectedBackgroundColor" |
|||
ResourceKey="SystemAccentColor" /> |
|||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" |
|||
ResourceKey="ListAccentLowOpacity" /> |
|||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundColor" |
|||
ResourceKey="SystemAccentColor" /> |
|||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" |
|||
ResourceKey="ListAccentMediumOpacity" /> |
|||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundColor" |
|||
ResourceKey="SystemAccentColor" /> |
|||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" |
|||
ResourceKey="ListAccentLowOpacity" /> |
|||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundColor" |
|||
ResourceKey="SystemAccentColor" /> |
|||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" |
|||
ResourceKey="ListAccentMediumOpacity" /> |
|||
<StaticResource x:Key="DataGridRowHeaderForegroundBrush" |
|||
ResourceKey="SystemControlForegroundBaseMediumBrush" /> |
|||
<StaticResource x:Key="DataGridRowHeaderBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundAltHighBrush" /> |
|||
<StaticResource x:Key="DataGridRowGroupHeaderBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundChromeMediumBrush" /> |
|||
<StaticResource x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundListLowBrush" /> |
|||
<StaticResource x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" |
|||
ResourceKey="SystemControlBackgroundListMediumBrush" /> |
|||
<StaticResource x:Key="DataGridRowGroupHeaderForegroundBrush" |
|||
ResourceKey="SystemControlForegroundBaseHighBrush" /> |
|||
<StaticResource x:Key="DataGridRowInvalidBrush" |
|||
ResourceKey="SystemErrorTextColor" /> |
|||
<StaticResource x:Key="DataGridCellBackgroundBrush" |
|||
ResourceKey="SystemControlTransparentBrush" /> |
|||
<StaticResource x:Key="DataGridCellFocusVisualPrimaryBrush" |
|||
ResourceKey="SystemControlFocusVisualPrimaryBrush" /> |
|||
<StaticResource x:Key="DataGridCellFocusVisualSecondaryBrush" |
|||
ResourceKey="SystemControlFocusVisualSecondaryBrush" /> |
|||
<StaticResource x:Key="DataGridCellInvalidBrush" |
|||
ResourceKey="SystemErrorTextColor" /> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="DataGridCell"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" /> |
|||
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> |
|||
<Setter Property="VerticalContentAlignment" Value="Stretch" /> |
|||
<Setter Property="FontSize" Value="15" /> |
|||
<Setter Property="MinHeight" Value="32" /> |
|||
<Setter Property="Focusable" Value="False" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid x:Name="PART_CellRoot" |
|||
ColumnDefinitions="*,Auto" |
|||
Background="{TemplateBinding Background}"> |
|||
|
|||
<Rectangle x:Name="CurrencyVisual" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}" |
|||
StrokeThickness="1" /> |
|||
<Grid x:Name="FocusVisual" |
|||
IsHitTestVisible="False"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}" |
|||
StrokeThickness="2" /> |
|||
<Rectangle Margin="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}" |
|||
StrokeThickness="1" /> |
|||
</Grid> |
|||
|
|||
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" |
|||
Content="{TemplateBinding Content}" |
|||
Margin="{TemplateBinding Padding}" |
|||
TextBlock.Foreground="{TemplateBinding Foreground}" |
|||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" |
|||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" /> |
|||
|
|||
<Rectangle x:Name="InvalidVisualElement" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellInvalidBrush}" |
|||
StrokeThickness="1" /> |
|||
|
|||
<Rectangle Name="PART_RightGridLine" |
|||
Grid.Column="1" |
|||
VerticalAlignment="Stretch" |
|||
Width="1" |
|||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" /> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridCell /template/ Rectangle#CurrencyVisual"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridCell /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridCell:current /template/ Rectangle#CurrencyVisual"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
<Style Selector="DataGrid:focus DataGridCell:current /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
<Style Selector="DataGridCell /template/ Rectangle#InvalidVisualElement"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader"> |
|||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" /> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" /> |
|||
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> |
|||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
|||
<Setter Property="Focusable" Value="False" /> |
|||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" /> |
|||
<Setter Property="Padding" Value="12,0,0,0" /> |
|||
<Setter Property="FontSize" Value="12" /> |
|||
<Setter Property="MinHeight" Value="32" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Name="PART_ColumnHeaderRoot" |
|||
ColumnDefinitions="*,Auto" |
|||
Background="{TemplateBinding Background}"> |
|||
|
|||
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" |
|||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" |
|||
Margin="{TemplateBinding Padding}"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*" /> |
|||
<ColumnDefinition MinWidth="32" |
|||
Width="Auto" /> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<ContentPresenter Content="{TemplateBinding Content}" /> |
|||
|
|||
<Path Name="SortIcon" |
|||
Grid.Column="1" |
|||
Fill="{TemplateBinding Foreground}" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
Stretch="Uniform" |
|||
Height="12" /> |
|||
</Grid> |
|||
|
|||
<Rectangle Name="VerticalSeparator" |
|||
Grid.Column="1" |
|||
Width="1" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{TemplateBinding SeparatorBrush}" |
|||
IsVisible="{TemplateBinding AreSeparatorsVisible}" /> |
|||
|
|||
<Grid x:Name="FocusVisual" |
|||
IsHitTestVisible="False"> |
|||
<Rectangle x:Name="FocusVisualPrimary" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}" |
|||
StrokeThickness="2" /> |
|||
<Rectangle x:Name="FocusVisualSecondary" |
|||
Margin="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}" |
|||
StrokeThickness="1" /> |
|||
</Grid> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridColumnHeader:focus-visible /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader:pointerover /template/ Grid#PART_ColumnHeaderRoot"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundColor}" /> |
|||
</Style> |
|||
<Style Selector="DataGridColumnHeader:pressed /template/ Grid#PART_ColumnHeaderRoot"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundColor}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader:dragIndicator"> |
|||
<Setter Property="Opacity" Value="0.5" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader /template/ Path#SortIcon"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRow"> |
|||
<Setter Property="Focusable" Value="False" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<DataGridFrozenGrid Name="PART_Root" |
|||
RowDefinitions="*,Auto,Auto" |
|||
ColumnDefinitions="Auto,*"> |
|||
|
|||
<Rectangle Name="BackgroundRectangle" |
|||
Grid.RowSpan="2" |
|||
Grid.ColumnSpan="2" /> |
|||
<Rectangle x:Name="InvalidVisualElement" |
|||
Grid.ColumnSpan="2" |
|||
Fill="{DynamicResource DataGridRowInvalidBrush}" /> |
|||
|
|||
<DataGridRowHeader Name="PART_RowHeader" |
|||
Grid.RowSpan="3" |
|||
DataGridFrozenGrid.IsFrozen="True" /> |
|||
<DataGridCellsPresenter Name="PART_CellsPresenter" |
|||
Grid.Column="1" |
|||
DataGridFrozenGrid.IsFrozen="True" /> |
|||
<DataGridDetailsPresenter Name="PART_DetailsPresenter" |
|||
Grid.Row="1" |
|||
Grid.Column="1" |
|||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" /> |
|||
<Rectangle Name="PART_BottomGridLine" |
|||
Grid.Row="2" |
|||
Grid.Column="1" |
|||
HorizontalAlignment="Stretch" |
|||
Height="1" /> |
|||
|
|||
</DataGridFrozenGrid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRow /template/ Rectangle#InvalidVisualElement"> |
|||
<Setter Property="Opacity" Value="0" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:invalid /template/ Rectangle#InvalidVisualElement"> |
|||
<Setter Property="Opacity" Value="0.4" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:invalid /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Opacity" Value="0" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource SystemControlTransparentBrush}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource SystemListLowColor}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:selected:focus /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:selected:pointerover:focus /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowHeader"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridRowHeaderBackgroundBrush}" /> |
|||
<Setter Property="Focusable" Value="False" /> |
|||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" /> |
|||
<Setter Property="AreSeparatorsVisible" Value="False" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid x:Name="PART_Root" |
|||
RowDefinitions="*,*,Auto" |
|||
ColumnDefinitions="Auto,*"> |
|||
<Border Grid.RowSpan="3" |
|||
Grid.ColumnSpan="2" |
|||
BorderBrush="{TemplateBinding SeparatorBrush}" |
|||
BorderThickness="0,0,1,0"> |
|||
<Grid Background="{TemplateBinding Background}"> |
|||
<Rectangle x:Name="RowInvalidVisualElement" |
|||
Fill="{DynamicResource DataGridRowInvalidBrush}" |
|||
Stretch="Fill" /> |
|||
<Rectangle x:Name="BackgroundRectangle" |
|||
Stretch="Fill" /> |
|||
</Grid> |
|||
</Border> |
|||
<Rectangle x:Name="HorizontalSeparator" |
|||
Grid.Row="2" |
|||
Grid.ColumnSpan="2" |
|||
Height="1" |
|||
Margin="1,0,1,0" |
|||
HorizontalAlignment="Stretch" |
|||
Fill="{TemplateBinding SeparatorBrush}" |
|||
IsVisible="{TemplateBinding AreSeparatorsVisible}" /> |
|||
|
|||
<ContentPresenter Grid.RowSpan="2" |
|||
Grid.Column="1" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
Content="{TemplateBinding Content}" /> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowHeader /template/ Rectangle#RowInvalidVisualElement"> |
|||
<Setter Property="Opacity" Value="0" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#RowInvalidVisualElement"> |
|||
<Setter Property="Opacity" Value="0.4" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Opacity" Value="0" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowHeader /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource SystemControlTransparentBrush}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:pointerover DataGridRowHeader /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource SystemListLowColor}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle"> |
|||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundColor}" /> |
|||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader"> |
|||
<Setter Property="Focusable" Value="False" /> |
|||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" /> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" /> |
|||
<Setter Property="FontSize" Value="15" /> |
|||
<Setter Property="MinHeight" Value="32" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<DataGridFrozenGrid Name="PART_Root" |
|||
MinHeight="{TemplateBinding MinHeight}" |
|||
ColumnDefinitions="Auto,Auto,Auto,Auto,*" |
|||
RowDefinitions="*,Auto"> |
|||
|
|||
<Rectangle Name="IndentSpacer" |
|||
Grid.Column="1" /> |
|||
<ToggleButton Name="ExpanderButton" |
|||
Grid.Column="2" |
|||
Width="12" |
|||
Height="12" |
|||
Margin="12,0,0,0" |
|||
Background="{TemplateBinding Background}" |
|||
Foreground="{TemplateBinding Foreground}" |
|||
Focusable="False" /> |
|||
|
|||
<StackPanel Grid.Column="3" |
|||
Orientation="Horizontal" |
|||
VerticalAlignment="Center" |
|||
Margin="12,0,0,0"> |
|||
<TextBlock Name="PropertyNameElement" |
|||
Margin="4,0,0,0" |
|||
IsVisible="{TemplateBinding IsPropertyNameVisible}" |
|||
Foreground="{TemplateBinding Foreground}" /> |
|||
<TextBlock Margin="4,0,0,0" |
|||
Text="{Binding Key}" |
|||
Foreground="{TemplateBinding Foreground}" /> |
|||
<TextBlock Name="ItemCountElement" |
|||
Margin="4,0,0,0" |
|||
IsVisible="{TemplateBinding IsItemCountVisible}" |
|||
Foreground="{TemplateBinding Foreground}" /> |
|||
</StackPanel> |
|||
|
|||
<Rectangle x:Name="CurrencyVisual" |
|||
Grid.ColumnSpan="5" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}" |
|||
StrokeThickness="1" /> |
|||
<Grid x:Name="FocusVisual" |
|||
Grid.ColumnSpan="5" |
|||
IsHitTestVisible="False"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}" |
|||
StrokeThickness="2" /> |
|||
<Rectangle Margin="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="Transparent" |
|||
IsHitTestVisible="False" |
|||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}" |
|||
StrokeThickness="1" /> |
|||
</Grid> |
|||
|
|||
<DataGridRowHeader Name="PART_RowHeader" |
|||
Grid.RowSpan="2" |
|||
DataGridFrozenGrid.IsFrozen="True" /> |
|||
|
|||
<Rectangle x:Name="PART_BottomGridLine" |
|||
Grid.Row="1" |
|||
Grid.ColumnSpan="5" |
|||
Height="1" /> |
|||
</DataGridFrozenGrid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Grid.Column="0" |
|||
Width="12" |
|||
Height="12" |
|||
Background="Transparent" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center"> |
|||
<Path Fill="{TemplateBinding Foreground}" |
|||
HorizontalAlignment="Right" |
|||
VerticalAlignment="Center" |
|||
Stretch="Uniform" /> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton /template/ Path"> |
|||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" /> |
|||
<Setter Property="Stretch" Value="Uniform" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path"> |
|||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconClosedPath}" /> |
|||
<Setter Property="Stretch" Value="UniformToFill" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader /template/ DataGridFrozenGrid#PART_Root"> |
|||
<Setter Property="Background" Value="{Binding $parent[DataGridRowGroupHeader].Background}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowGroupHeader:pointerover /template/ DataGridFrozenGrid#PART_Root"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderHoveredBackgroundBrush}" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowGroupHeader:pressed /template/ DataGridFrozenGrid#PART_Root"> |
|||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderPressedBackgroundBrush}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGridRowGroupHeader /template/ Rectangle#CurrencyVisual"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowGroupHeader /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="DataGridRowGroupHeader:current /template/ Rectangle#CurrencyVisual"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
<Style Selector="DataGrid:focus DataGridRowGroupHeader:current /template/ Grid#FocusVisual"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
|
|||
<Style Selector="DataGrid"> |
|||
<Setter Property="RowBackground" Value="Transparent" /> |
|||
<Setter Property="AlternatingRowBackground" Value="Transparent" /> |
|||
<Setter Property="HeadersVisibility" Value="Column" /> |
|||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" /> |
|||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" /> |
|||
<Setter Property="SelectionMode" Value="Extended" /> |
|||
<Setter Property="GridLinesVisibility" Value="None" /> |
|||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" /> |
|||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" /> |
|||
<Setter Property="DropLocationIndicatorTemplate"> |
|||
<Template> |
|||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}" |
|||
Width="2" /> |
|||
</Template> |
|||
</Setter> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}"> |
|||
<Grid RowDefinitions="Auto,*,Auto,Auto" |
|||
ColumnDefinitions="Auto,*,Auto"> |
|||
<Grid.Resources> |
|||
<ControlTemplate x:Key="TopLeftHeaderTemplate" |
|||
TargetType="DataGridColumnHeader"> |
|||
<Grid x:Name="TopLeftHeaderRoot" |
|||
RowDefinitions="*,*,Auto"> |
|||
<Border Grid.RowSpan="2" |
|||
BorderThickness="0,0,1,0" |
|||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" /> |
|||
<Rectangle Grid.RowSpan="2" |
|||
VerticalAlignment="Bottom" |
|||
StrokeThickness="1" |
|||
Height="1" |
|||
Fill="{DynamicResource DataGridGridLinesBrush}" /> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
<ControlTemplate x:Key="TopRightHeaderTemplate" |
|||
TargetType="DataGridColumnHeader"> |
|||
<Grid x:Name="RootElement" /> |
|||
</ControlTemplate> |
|||
</Grid.Resources> |
|||
|
|||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" |
|||
Template="{StaticResource TopLeftHeaderTemplate}" /> |
|||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" |
|||
Grid.Column="1" |
|||
Grid.ColumnSpan="2" /> |
|||
<!--<DataGridColumnHeader Name="PART_TopRightCornerHeader" |
|||
Grid.Column="2" |
|||
Template="{StaticResource TopRightHeaderTemplate}" />--> |
|||
<!--<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" |
|||
Grid.ColumnSpan="3" |
|||
VerticalAlignment="Bottom" |
|||
StrokeThickness="1" |
|||
Height="1" |
|||
Fill="{DynamicResource DataGridGridLinesBrush}" />--> |
|||
<Border Name="PART_ColumnHeadersAndRowsSeparator" |
|||
Grid.ColumnSpan="3" |
|||
Height="2" |
|||
VerticalAlignment="Bottom" |
|||
BorderThickness="0,0,0,1" |
|||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" /> |
|||
|
|||
<DataGridRowsPresenter Name="PART_RowsPresenter" |
|||
Grid.Row="1" |
|||
Grid.RowSpan="2" |
|||
Grid.ColumnSpan="3" /> |
|||
<Rectangle Name="PART_BottomRightCorner" |
|||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}" |
|||
Grid.Column="2" |
|||
Grid.Row="2" /> |
|||
<!--<Rectangle Name="BottomLeftCorner" |
|||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}" |
|||
Grid.Row="2" |
|||
Grid.ColumnSpan="2" />--> |
|||
<ScrollBar Name="PART_VerticalScrollbar" |
|||
Orientation="Vertical" |
|||
Grid.Column="2" |
|||
Grid.Row="1" |
|||
Width="{DynamicResource ScrollBarSize}" /> |
|||
|
|||
<Grid Grid.Column="1" |
|||
Grid.Row="2" |
|||
ColumnDefinitions="Auto,*"> |
|||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" /> |
|||
<ScrollBar Name="PART_HorizontalScrollbar" |
|||
Grid.Column="1" |
|||
Orientation="Horizontal" |
|||
Height="{DynamicResource ScrollBarSize}" /> |
|||
</Grid> |
|||
<Border x:Name="PART_DisabledVisualElement" |
|||
Grid.ColumnSpan="3" |
|||
Grid.RowSpan="4" |
|||
IsHitTestVisible="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
CornerRadius="2" |
|||
Background="{DynamicResource DataGridDisabledVisualElementBackground}" |
|||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" /> |
|||
</Grid> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
</Styles> |
|||
@ -0,0 +1,18 @@ |
|||
Compat issues with assembly Avalonia.Controls: |
|||
TypesMustExist : Type 'Avalonia.Controls.IndexPath' does not exist in the implementation but it does exist in the contract. |
|||
TypesMustExist : Type 'Avalonia.Controls.ISelectedItemInfo' does not exist in the implementation but it does exist in the contract. |
|||
TypesMustExist : Type 'Avalonia.Controls.ISelectionModel' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.ListBox.SelectionProperty' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.ListBox.Selection.get()' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public void Avalonia.Controls.ListBox.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. |
|||
TypesMustExist : Type 'Avalonia.Controls.SelectionModel' does not exist in the implementation but it does exist in the contract. |
|||
TypesMustExist : Type 'Avalonia.Controls.SelectionModelChildrenRequestedEventArgs' does not exist in the implementation but it does exist in the contract. |
|||
TypesMustExist : Type 'Avalonia.Controls.SelectionModelSelectionChangedEventArgs' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.Controls.TreeView.SelectionProperty' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. |
|||
Total Issues: 16 |
|||
@ -1,249 +0,0 @@ |
|||
// 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.ComponentModel; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the selected items for a control.
|
|||
/// </summary>
|
|||
public interface ISelectionModel : INotifyPropertyChanged |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the anchor index.
|
|||
/// </summary>
|
|||
IndexPath AnchorIndex { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or set the index of the first selected item.
|
|||
/// </summary>
|
|||
IndexPath SelectedIndex { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or set the indexes of the selected items.
|
|||
/// </summary>
|
|||
IReadOnlyList<IndexPath> SelectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the first selected item.
|
|||
/// </summary>
|
|||
object SelectedItem { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the selected items.
|
|||
/// </summary>
|
|||
IReadOnlyList<object> SelectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the model represents a single or multiple selection.
|
|||
/// </summary>
|
|||
bool SingleSelect { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether to always keep an item selected where possible.
|
|||
/// </summary>
|
|||
bool AutoSelect { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the collection that contains the items that can be selected.
|
|||
/// </summary>
|
|||
object Source { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Raised when the children of a selection are required.
|
|||
/// </summary>
|
|||
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested; |
|||
|
|||
/// <summary>
|
|||
/// Raised when the selection has changed.
|
|||
/// </summary>
|
|||
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged; |
|||
|
|||
/// <summary>
|
|||
/// Clears the selection.
|
|||
/// </summary>
|
|||
void ClearSelection(); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
void Deselect(int index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
void Deselect(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
void DeselectAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items.
|
|||
/// </summary>
|
|||
/// <param name="start">The start index of the range.</param>
|
|||
/// <param name="end">The end index of the range.</param>
|
|||
void DeselectRange(IndexPath start, IndexPath end); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void DeselectRangeFromAnchor(int index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="endGroupIndex">
|
|||
/// The index of the item group that represents the end of the selection.
|
|||
/// </param>
|
|||
/// <param name="endItemIndex">
|
|||
/// The index of the item in the group that represents the end of the selection.
|
|||
/// </param>
|
|||
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void DeselectRangeFromAnchorTo(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Disposes the object and clears the selection.
|
|||
/// </summary>
|
|||
void Dispose(); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
bool IsSelected(int index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
bool IsSelected(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
public bool IsSelectedAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartial(int index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartial(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartialAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
void Select(int index); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
void Select(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
void SelectAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Selects all items.
|
|||
/// </summary>
|
|||
void SelectAll(); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items.
|
|||
/// </summary>
|
|||
/// <param name="start">The start index of the range.</param>
|
|||
/// <param name="end">The end index of the range.</param>
|
|||
void SelectRange(IndexPath start, IndexPath end); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void SelectRangeFromAnchor(int index); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="endGroupIndex">
|
|||
/// The index of the item group that represents the end of the selection.
|
|||
/// </param>
|
|||
/// <param name="endItemIndex">
|
|||
/// The index of the item in the group that represents the end of the selection.
|
|||
/// </param>
|
|||
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void SelectRangeFromAnchorTo(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Sets the <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The anchor index.</param>
|
|||
void SetAnchorIndex(int index); |
|||
|
|||
/// <summary>
|
|||
/// Sets the <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="index">The index of the item in the group.</param>
|
|||
void SetAnchorIndex(int groupIndex, int index); |
|||
|
|||
/// <summary>
|
|||
/// Begins a batch update of the selection.
|
|||
/// </summary>
|
|||
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
|
|||
IDisposable Update(); |
|||
} |
|||
} |
|||
@ -1,200 +0,0 @@ |
|||
// 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; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath> |
|||
{ |
|||
public static readonly IndexPath Unselected = default; |
|||
|
|||
private readonly int _index; |
|||
private readonly int[]? _path; |
|||
|
|||
public IndexPath(int index) |
|||
{ |
|||
_index = index + 1; |
|||
_path = null; |
|||
} |
|||
|
|||
public IndexPath(int groupIndex, int itemIndex) |
|||
{ |
|||
_index = 0; |
|||
_path = new[] { groupIndex, itemIndex }; |
|||
} |
|||
|
|||
public IndexPath(IEnumerable<int>? indices) |
|||
{ |
|||
if (indices != null) |
|||
{ |
|||
_index = 0; |
|||
_path = indices.ToArray(); |
|||
} |
|||
else |
|||
{ |
|||
_index = 0; |
|||
_path = null; |
|||
} |
|||
} |
|||
|
|||
private IndexPath(int[] basePath, int index) |
|||
{ |
|||
basePath = basePath ?? throw new ArgumentNullException(nameof(basePath)); |
|||
|
|||
_index = 0; |
|||
_path = new int[basePath.Length + 1]; |
|||
Array.Copy(basePath, _path, basePath.Length); |
|||
_path[basePath.Length] = index; |
|||
} |
|||
|
|||
public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1); |
|||
|
|||
public int GetAt(int index) |
|||
{ |
|||
if (index >= GetSize()) |
|||
{ |
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
|
|||
return _path?[index] ?? (_index - 1); |
|||
} |
|||
|
|||
public int CompareTo(IndexPath other) |
|||
{ |
|||
var rhsPath = other; |
|||
int compareResult = 0; |
|||
int lhsCount = GetSize(); |
|||
int rhsCount = rhsPath.GetSize(); |
|||
|
|||
if (lhsCount == 0 || rhsCount == 0) |
|||
{ |
|||
// one of the paths are empty, compare based on size
|
|||
compareResult = (lhsCount - rhsCount); |
|||
} |
|||
else |
|||
{ |
|||
// both paths are non-empty, but can be of different size
|
|||
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++) |
|||
{ |
|||
if (GetAt(i) < rhsPath.GetAt(i)) |
|||
{ |
|||
compareResult = -1; |
|||
break; |
|||
} |
|||
else if (GetAt(i) > rhsPath.GetAt(i)) |
|||
{ |
|||
compareResult = 1; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// if both match upto min(lhsCount, rhsCount), compare based on size
|
|||
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult; |
|||
} |
|||
|
|||
if (compareResult != 0) |
|||
{ |
|||
compareResult = compareResult > 0 ? 1 : -1; |
|||
} |
|||
|
|||
return compareResult; |
|||
} |
|||
|
|||
public IndexPath CloneWithChildIndex(int childIndex) |
|||
{ |
|||
if (_path != null) |
|||
{ |
|||
return new IndexPath(_path, childIndex); |
|||
} |
|||
else if (_index != 0) |
|||
{ |
|||
return new IndexPath(_index - 1, childIndex); |
|||
} |
|||
else |
|||
{ |
|||
return new IndexPath(childIndex); |
|||
} |
|||
} |
|||
|
|||
public bool IsAncestorOf(in IndexPath other) |
|||
{ |
|||
if (other.GetSize() <= GetSize()) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var size = GetSize(); |
|||
|
|||
for (int i = 0; i < size; i++) |
|||
{ |
|||
if (GetAt(i) != other.GetAt(i)) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
if (_path != null) |
|||
{ |
|||
return "R" + string.Join(".", _path); |
|||
} |
|||
else if (_index != 0) |
|||
{ |
|||
return "R" + (_index - 1); |
|||
} |
|||
else |
|||
{ |
|||
return "R"; |
|||
} |
|||
} |
|||
|
|||
public static IndexPath CreateFrom(int index) => new IndexPath(index); |
|||
|
|||
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex); |
|||
|
|||
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices); |
|||
|
|||
public override bool Equals(object obj) => obj is IndexPath other && Equals(other); |
|||
|
|||
public bool Equals(IndexPath other) => CompareTo(other) == 0; |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
var hashCode = -504981047; |
|||
|
|||
if (_path != null) |
|||
{ |
|||
foreach (var i in _path) |
|||
{ |
|||
hashCode = hashCode * -1521134295 + i.GetHashCode(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
hashCode = hashCode * -1521134295 + _index.GetHashCode(); |
|||
} |
|||
|
|||
return hashCode; |
|||
} |
|||
|
|||
public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; } |
|||
public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; } |
|||
public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; } |
|||
public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; } |
|||
public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; } |
|||
public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; } |
|||
public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; } |
|||
public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; } |
|||
} |
|||
} |
|||
@ -1,49 +0,0 @@ |
|||
// 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; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public interface ISelectedItemInfo |
|||
{ |
|||
public IndexPath Path { get; } |
|||
} |
|||
|
|||
internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue> |
|||
where Tinfo : ISelectedItemInfo |
|||
{ |
|||
private readonly List<Tinfo> _infos; |
|||
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl; |
|||
|
|||
public SelectedItems( |
|||
List<Tinfo> infos, |
|||
int count, |
|||
Func<List<Tinfo>, int, TValue> getAtImpl) |
|||
{ |
|||
_infos = infos; |
|||
_getAtImpl = getAtImpl; |
|||
Count = count; |
|||
} |
|||
|
|||
public TValue this[int index] => _getAtImpl(_infos, index); |
|||
|
|||
public int Count { get; } |
|||
|
|||
public IEnumerator<TValue> GetEnumerator() |
|||
{ |
|||
for (var i = 0; i < Count; ++i) |
|||
{ |
|||
yield return this[i]; |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public interface ISelectionModel : INotifyPropertyChanged |
|||
{ |
|||
IEnumerable? Source { get; set; } |
|||
bool SingleSelect { get; set; } |
|||
int SelectedIndex { get; set; } |
|||
IReadOnlyList<int> SelectedIndexes { get; } |
|||
object? SelectedItem { get; set; } |
|||
IReadOnlyList<object?> SelectedItems { get; } |
|||
int AnchorIndex { get; set; } |
|||
int Count { get; } |
|||
|
|||
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged; |
|||
public event EventHandler? LostSelection; |
|||
public event EventHandler? SourceReset; |
|||
|
|||
public void BeginBatchUpdate(); |
|||
public void EndBatchUpdate(); |
|||
bool IsSelected(int index); |
|||
void Select(int index); |
|||
void Deselect(int index); |
|||
void SelectRange(int start, int end); |
|||
void DeselectRange(int start, int end); |
|||
void SelectAll(); |
|||
void Clear(); |
|||
} |
|||
|
|||
public static class SelectionModelExtensions |
|||
{ |
|||
public static IDisposable BatchUpdate(this ISelectionModel model) |
|||
{ |
|||
return new BatchUpdateOperation(model); |
|||
} |
|||
|
|||
public struct BatchUpdateOperation : IDisposable |
|||
{ |
|||
private readonly ISelectionModel _owner; |
|||
private bool _isDisposed; |
|||
|
|||
public BatchUpdateOperation(ISelectionModel owner) |
|||
{ |
|||
_owner = owner; |
|||
_isDisposed = false; |
|||
owner.BeginBatchUpdate(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!_isDisposed) |
|||
{ |
|||
_owner?.EndBatchUpdate(); |
|||
_isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
internal class SelectedIndexes<T> : IReadOnlyList<int> |
|||
{ |
|||
private readonly SelectionModel<T>? _owner; |
|||
private readonly IReadOnlyList<IndexRange>? _ranges; |
|||
|
|||
public SelectedIndexes(SelectionModel<T> owner) => _owner = owner; |
|||
public SelectedIndexes(IReadOnlyList<IndexRange> ranges) => _ranges = ranges; |
|||
|
|||
public int this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
if (index >= Count) |
|||
{ |
|||
throw new IndexOutOfRangeException("The index was out of range."); |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetAt(Ranges!, index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex == -1 ? 0 : 1; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetCount(Ranges!); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!; |
|||
|
|||
public IEnumerator<int> GetEnumerator() |
|||
{ |
|||
IEnumerator<int> SingleSelect() |
|||
{ |
|||
if (_owner.SelectedIndex >= 0) |
|||
{ |
|||
yield return _owner.SelectedIndex; |
|||
} |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return SingleSelect(); |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.EnumerateIndices(Ranges).GetEnumerator(); |
|||
} |
|||
} |
|||
|
|||
public static SelectedIndexes<T>? Create(IReadOnlyList<IndexRange>? ranges) |
|||
{ |
|||
return ranges is object ? new SelectedIndexes<T>(ranges) : null; |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
internal class SelectedItems<T> : IReadOnlyList<T> |
|||
{ |
|||
private readonly SelectionModel<T>? _owner; |
|||
private readonly ItemsSourceView<T>? _items; |
|||
private readonly IReadOnlyList<IndexRange>? _ranges; |
|||
|
|||
public SelectedItems(SelectionModel<T> owner) => _owner = owner; |
|||
|
|||
public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items) |
|||
{ |
|||
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges)); |
|||
_items = items; |
|||
} |
|||
|
|||
[MaybeNull] |
|||
public T this[int index] |
|||
{ |
|||
#pragma warning disable CS8766
|
|||
get |
|||
#pragma warning restore CS8766
|
|||
{ |
|||
if (index >= Count) |
|||
{ |
|||
throw new IndexOutOfRangeException("The index was out of range."); |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedItem; |
|||
} |
|||
else if (Items is object) |
|||
{ |
|||
return Items[index]; |
|||
} |
|||
else |
|||
{ |
|||
return default; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex == -1 ? 0 : 1; |
|||
} |
|||
else |
|||
{ |
|||
return Ranges is object ? IndexRange.GetCount(Ranges) : 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView; |
|||
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges; |
|||
|
|||
public IEnumerator<T> GetEnumerator() |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
if (_owner.SelectedIndex >= 0) |
|||
{ |
|||
#pragma warning disable CS8603
|
|||
yield return _owner.SelectedItem; |
|||
#pragma warning restore CS8603
|
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var items = Items; |
|||
|
|||
foreach (var range in Ranges!) |
|||
{ |
|||
for (var i = range.Begin; i <= range.End; ++i) |
|||
{ |
|||
#pragma warning disable CS8603
|
|||
yield return items is object ? items[i] : default; |
|||
#pragma warning restore CS8603
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public static SelectedItems<T>? Create( |
|||
IReadOnlyList<IndexRange>? ranges, |
|||
ItemsSourceView<T>? items) |
|||
{ |
|||
return ranges is object ? new SelectedItems<T>(ranges, items) : null; |
|||
} |
|||
|
|||
public class Untyped : IReadOnlyList<object?> |
|||
{ |
|||
private readonly IReadOnlyList<T> _source; |
|||
public Untyped(IReadOnlyList<T> source) => _source = source; |
|||
public object? this[int index] => _source[index]; |
|||
public int Count => _source.Count; |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
public IEnumerator<object?> GetEnumerator() |
|||
{ |
|||
foreach (var i in _source) |
|||
{ |
|||
yield return i; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,726 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public class SelectionModel<T> : SelectionNodeBase<T>, ISelectionModel |
|||
{ |
|||
private bool _singleSelect = true; |
|||
private int _anchorIndex = -1; |
|||
private int _selectedIndex = -1; |
|||
private Operation? _operation; |
|||
private SelectedIndexes<T>? _selectedIndexes; |
|||
private SelectedItems<T>? _selectedItems; |
|||
private SelectedItems<T>.Untyped? _selectedItemsUntyped; |
|||
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged; |
|||
[AllowNull] private T _initSelectedItem = default; |
|||
private bool _hasInitSelectedItem; |
|||
|
|||
public SelectionModel() |
|||
{ |
|||
} |
|||
|
|||
public SelectionModel(IEnumerable<T>? source) |
|||
{ |
|||
Source = source; |
|||
} |
|||
|
|||
public new IEnumerable<T>? Source |
|||
{ |
|||
get => base.Source as IEnumerable<T>; |
|||
set => SetSource(value); |
|||
} |
|||
|
|||
public bool SingleSelect |
|||
{ |
|||
get => _singleSelect; |
|||
set |
|||
{ |
|||
if (_singleSelect != value) |
|||
{ |
|||
if (value == true) |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
var selectedIndex = SelectedIndex; |
|||
Clear(); |
|||
SelectedIndex = selectedIndex; |
|||
} |
|||
|
|||
_singleSelect = value; |
|||
RangesEnabled = !value; |
|||
|
|||
if (RangesEnabled && _selectedIndex >= 0) |
|||
{ |
|||
CommitSelect(new IndexRange(_selectedIndex)); |
|||
} |
|||
|
|||
RaisePropertyChanged(nameof(SingleSelect)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int SelectedIndex |
|||
{ |
|||
get => _selectedIndex; |
|||
set |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
Clear(); |
|||
Select(value); |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<int> SelectedIndexes => _selectedIndexes ??= new SelectedIndexes<T>(this); |
|||
|
|||
[MaybeNull, AllowNull] |
|||
public T SelectedItem |
|||
{ |
|||
get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; |
|||
set |
|||
{ |
|||
if (ItemsView is object) |
|||
{ |
|||
SelectedIndex = ItemsView.IndexOf(value!); |
|||
} |
|||
else |
|||
{ |
|||
Clear(); |
|||
_initSelectedItem = value; |
|||
_hasInitSelectedItem = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<T> SelectedItems |
|||
{ |
|||
get |
|||
{ |
|||
if (ItemsView is null && _hasInitSelectedItem) |
|||
{ |
|||
return new[] { _initSelectedItem }; |
|||
} |
|||
|
|||
return _selectedItems ??= new SelectedItems<T>(this); |
|||
} |
|||
} |
|||
|
|||
public int AnchorIndex |
|||
{ |
|||
get => _anchorIndex; |
|||
set |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
var index = CoerceIndex(value); |
|||
update.Operation.AnchorIndex = index; |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (SingleSelect) |
|||
{ |
|||
return _selectedIndex >= 0 ? 1 : 0; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetCount(Ranges); |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerable? ISelectionModel.Source |
|||
{ |
|||
get => Source; |
|||
set => SetSource(value); |
|||
} |
|||
|
|||
object? ISelectionModel.SelectedItem |
|||
{ |
|||
get => SelectedItem; |
|||
set |
|||
{ |
|||
if (value is T t) |
|||
{ |
|||
SelectedItem = t; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex = -1; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
IReadOnlyList<object?> ISelectionModel.SelectedItems |
|||
{ |
|||
get => _selectedItemsUntyped ??= new SelectedItems<T>.Untyped(SelectedItems); |
|||
} |
|||
|
|||
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs<T>>? SelectionChanged; |
|||
public event EventHandler? LostSelection; |
|||
public event EventHandler? SourceReset; |
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
|
|||
event EventHandler<SelectionModelSelectionChangedEventArgs>? ISelectionModel.SelectionChanged |
|||
{ |
|||
add => _untypedSelectionChanged += value; |
|||
remove => _untypedSelectionChanged -= value; |
|||
} |
|||
|
|||
public BatchUpdateOperation BatchUpdate() => new BatchUpdateOperation(this); |
|||
|
|||
public void BeginBatchUpdate() |
|||
{ |
|||
_operation ??= new Operation(this); |
|||
++_operation.UpdateCount; |
|||
} |
|||
|
|||
public void EndBatchUpdate() |
|||
{ |
|||
if (_operation is null || _operation.UpdateCount == 0) |
|||
{ |
|||
throw new InvalidOperationException("No batch update in progress."); |
|||
} |
|||
|
|||
if (--_operation.UpdateCount == 0) |
|||
{ |
|||
// If the collection is currently changing, commit the update when the
|
|||
// collection change finishes.
|
|||
if (!IsSourceCollectionChanging) |
|||
{ |
|||
CommitOperation(_operation); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool IsSelected(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
return false; |
|||
} |
|||
else if (SingleSelect) |
|||
{ |
|||
return _selectedIndex == index; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.Contains(Ranges, index); |
|||
} |
|||
} |
|||
|
|||
public void Select(int index) => SelectRange(index, index, false, true); |
|||
|
|||
public void Deselect(int index) => DeselectRange(index, index); |
|||
|
|||
public void SelectRange(int start, int end) => SelectRange(start, end, false, false); |
|||
|
|||
public void DeselectRange(int start, int end) |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
var o = update.Operation; |
|||
var range = CoerceRange(start, end); |
|||
|
|||
if (range.Begin == -1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (RangesEnabled) |
|||
{ |
|||
var selected = Ranges.ToList(); |
|||
var deselected = new List<IndexRange>(); |
|||
var operationDeselected = new List<IndexRange>(); |
|||
|
|||
o.DeselectedRanges ??= new List<IndexRange>(); |
|||
IndexRange.Remove(o.SelectedRanges, range, operationDeselected); |
|||
IndexRange.Remove(selected, range, deselected); |
|||
IndexRange.Add(o.DeselectedRanges, deselected); |
|||
|
|||
if (IndexRange.Contains(deselected, o.SelectedIndex) || |
|||
IndexRange.Contains(operationDeselected, o.SelectedIndex)) |
|||
{ |
|||
o.SelectedIndex = GetFirstSelectedIndexFromRanges(except: deselected); |
|||
} |
|||
} |
|||
else if(range.Contains(_selectedIndex)) |
|||
{ |
|||
o.SelectedIndex = -1; |
|||
} |
|||
|
|||
_initSelectedItem = default; |
|||
_hasInitSelectedItem = false; |
|||
} |
|||
|
|||
public void SelectAll() => SelectRange(0, int.MaxValue); |
|||
public void Clear() => DeselectRange(0, int.MaxValue); |
|||
|
|||
protected void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
|
|||
private void SetSource(IEnumerable? value) |
|||
{ |
|||
if (base.Source != value) |
|||
{ |
|||
if (_operation is object) |
|||
{ |
|||
throw new InvalidOperationException("Cannot change source while update is in progress."); |
|||
} |
|||
|
|||
if (base.Source is object && value is object) |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
update.Operation.SkipLostSelection = true; |
|||
Clear(); |
|||
} |
|||
|
|||
base.Source = value; |
|||
|
|||
using (var update = BatchUpdate()) |
|||
{ |
|||
update.Operation.IsSourceUpdate = true; |
|||
|
|||
if (_hasInitSelectedItem) |
|||
{ |
|||
SelectedItem = _initSelectedItem; |
|||
_initSelectedItem = default; |
|||
_hasInitSelectedItem = false; |
|||
} |
|||
else |
|||
{ |
|||
TrimInvalidSelections(update.Operation); |
|||
} |
|||
|
|||
RaisePropertyChanged(nameof(Source)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) |
|||
{ |
|||
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta)); |
|||
} |
|||
|
|||
private protected override void OnSourceReset() |
|||
{ |
|||
_selectedIndex = _anchorIndex = -1; |
|||
CommitDeselect(new IndexRange(0, int.MaxValue)); |
|||
|
|||
if (SourceReset is object) |
|||
{ |
|||
SourceReset.Invoke(this, EventArgs.Empty); |
|||
} |
|||
else |
|||
{ |
|||
//Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
|
|||
// this,
|
|||
// "SelectionModel received Reset but no SourceReset handler was registered to handle it. " +
|
|||
// "Selection may be out of sync.",
|
|||
// typeof(SelectionModel));
|
|||
} |
|||
} |
|||
|
|||
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems) |
|||
{ |
|||
// Note: We're *not* putting this in a using scope. A collection update is still in progress
|
|||
// so the operation won't get commited by normal means: we have to commit it manually.
|
|||
var update = BatchUpdate(); |
|||
|
|||
update.Operation.DeselectedItems = deselectedItems; |
|||
|
|||
if (_selectedIndex == -1 && LostSelection is object) |
|||
{ |
|||
LostSelection(this, EventArgs.Empty); |
|||
} |
|||
|
|||
CommitOperation(update.Operation); |
|||
} |
|||
|
|||
private protected override CollectionChangeState OnItemsAdded(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var shifted = SelectedIndex >= index; |
|||
var shiftCount = shifted ? count : 0; |
|||
|
|||
_selectedIndex += shiftCount; |
|||
_anchorIndex += shiftCount; |
|||
|
|||
var baseResult = base.OnItemsAdded(index, items); |
|||
shifted |= baseResult.ShiftDelta != 0; |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? count : 0, |
|||
}; |
|||
} |
|||
|
|||
private protected override CollectionChangeState OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var removedRange = new IndexRange(index, index + count - 1); |
|||
var shifted = false; |
|||
List<T>? removed; |
|||
|
|||
var baseResult = base.OnItemsRemoved(index, items); |
|||
shifted |= baseResult.ShiftDelta != 0; |
|||
removed = baseResult.RemovedItems; |
|||
|
|||
if (removedRange.Contains(SelectedIndex)) |
|||
{ |
|||
if (SingleSelect) |
|||
{ |
|||
#pragma warning disable CS8604
|
|||
removed = new List<T> { (T)items[SelectedIndex - index] }; |
|||
#pragma warning restore CS8604
|
|||
} |
|||
|
|||
_selectedIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
else if (SelectedIndex >= index) |
|||
{ |
|||
_selectedIndex -= count; |
|||
shifted = true; |
|||
} |
|||
|
|||
if (removedRange.Contains(AnchorIndex)) |
|||
{ |
|||
_anchorIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
else if (AnchorIndex >= index) |
|||
{ |
|||
_anchorIndex -= count; |
|||
shifted = true; |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? -count : 0, |
|||
RemovedItems = removed, |
|||
}; |
|||
} |
|||
|
|||
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_operation?.UpdateCount > 0) |
|||
{ |
|||
throw new InvalidOperationException("Source collection was modified during selection update."); |
|||
} |
|||
|
|||
var oldAnchorIndex = _anchorIndex; |
|||
var oldSelectedIndex = _selectedIndex; |
|||
|
|||
base.OnSourceCollectionChanged(e); |
|||
|
|||
if (oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
} |
|||
|
|||
if (oldAnchorIndex != _anchorIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
} |
|||
|
|||
protected override void OnSourceCollectionChangeFinished() |
|||
{ |
|||
if (_operation is object) |
|||
{ |
|||
CommitOperation(_operation); |
|||
} |
|||
} |
|||
|
|||
private int GetFirstSelectedIndexFromRanges(List<IndexRange>? except = null) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
var count = IndexRange.GetCount(Ranges); |
|||
var index = 0; |
|||
|
|||
while (index < count) |
|||
{ |
|||
var result = IndexRange.GetAt(Ranges, index++); |
|||
|
|||
if (!IndexRange.Contains(except, result)) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return -1; |
|||
} |
|||
|
|||
private void SelectRange( |
|||
int start, |
|||
int end, |
|||
bool forceSelectedIndex, |
|||
bool forceAnchorIndex) |
|||
{ |
|||
if (SingleSelect && start != end) |
|||
{ |
|||
throw new InvalidOperationException("Cannot select range with single selection."); |
|||
} |
|||
|
|||
var range = CoerceRange(start, end); |
|||
|
|||
if (range.Begin == -1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
using var update = BatchUpdate(); |
|||
var o = update.Operation; |
|||
var selected = new List<IndexRange>(); |
|||
|
|||
if (RangesEnabled) |
|||
{ |
|||
o.SelectedRanges ??= new List<IndexRange>(); |
|||
IndexRange.Remove(o.DeselectedRanges, range); |
|||
IndexRange.Add(o.SelectedRanges, range); |
|||
IndexRange.Remove(o.SelectedRanges, Ranges); |
|||
|
|||
if (o.SelectedIndex == -1 || forceSelectedIndex) |
|||
{ |
|||
o.SelectedIndex = range.Begin; |
|||
} |
|||
|
|||
if (o.AnchorIndex == -1 || forceAnchorIndex) |
|||
{ |
|||
o.AnchorIndex = range.Begin; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
o.SelectedIndex = o.AnchorIndex = start; |
|||
} |
|||
|
|||
_initSelectedItem = default; |
|||
_hasInitSelectedItem = false; |
|||
} |
|||
|
|||
[return: MaybeNull] |
|||
private T GetItemAt(int index) |
|||
{ |
|||
if (ItemsView is null || index < 0 || index >= ItemsView.Count) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
return ItemsView[index]; |
|||
} |
|||
|
|||
private int CoerceIndex(int index) |
|||
{ |
|||
index = Math.Max(index, -1); |
|||
|
|||
if (ItemsView is object && index >= ItemsView.Count) |
|||
{ |
|||
index = -1; |
|||
} |
|||
|
|||
return index; |
|||
} |
|||
|
|||
private IndexRange CoerceRange(int start, int end) |
|||
{ |
|||
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue; |
|||
|
|||
if (start > max || (start < 0 && end < 0)) |
|||
{ |
|||
return new IndexRange(-1); |
|||
} |
|||
|
|||
start = Math.Max(start, 0); |
|||
end = Math.Min(end, max); |
|||
|
|||
return new IndexRange(start, end); |
|||
} |
|||
|
|||
private void TrimInvalidSelections(Operation operation) |
|||
{ |
|||
if (ItemsView is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var max = ItemsView.Count - 1; |
|||
|
|||
if (operation.SelectedIndex > max) |
|||
{ |
|||
operation.SelectedIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
|
|||
if (operation.AnchorIndex > max) |
|||
{ |
|||
operation.AnchorIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
|
|||
if (RangesEnabled && Ranges.Count > 0) |
|||
{ |
|||
var selected = Ranges.ToList(); |
|||
|
|||
if (max < 0) |
|||
{ |
|||
operation.DeselectedRanges = selected; |
|||
} |
|||
else |
|||
{ |
|||
var valid = new IndexRange(0, max); |
|||
var removed = new List<IndexRange>(); |
|||
IndexRange.Intersect(selected, valid, removed); |
|||
operation.DeselectedRanges = removed; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void CommitOperation(Operation operation) |
|||
{ |
|||
try |
|||
{ |
|||
var oldAnchorIndex = _anchorIndex; |
|||
var oldSelectedIndex = _selectedIndex; |
|||
var indexesChanged = false; |
|||
|
|||
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection) |
|||
{ |
|||
operation.UpdateCount++; |
|||
LostSelection?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
|
|||
_selectedIndex = operation.SelectedIndex; |
|||
_anchorIndex = operation.AnchorIndex; |
|||
|
|||
if (operation.SelectedRanges is object) |
|||
{ |
|||
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0; |
|||
} |
|||
|
|||
if (operation.DeselectedRanges is object) |
|||
{ |
|||
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0; |
|||
} |
|||
|
|||
if (SelectionChanged is object || _untypedSelectionChanged is object) |
|||
{ |
|||
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges; |
|||
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges; |
|||
|
|||
if (SingleSelect && oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
if (oldSelectedIndex != -1) |
|||
{ |
|||
deselected = new[] { new IndexRange(oldSelectedIndex) }; |
|||
} |
|||
|
|||
if (_selectedIndex != -1) |
|||
{ |
|||
selected = new[] { new IndexRange(_selectedIndex) }; |
|||
} |
|||
} |
|||
|
|||
if (deselected?.Count > 0 || selected?.Count > 0 || operation.DeselectedItems is object) |
|||
{ |
|||
// If the operation was caused by Source being updated, then use a null source
|
|||
// so that the items will appear as nulls.
|
|||
var deselectedSource = operation.IsSourceUpdate ? null : ItemsView; |
|||
|
|||
// If the operation contains DeselectedItems then we're notifying a source
|
|||
// CollectionChanged event. LostFocus may have caused another item to have been
|
|||
// selected, but it can't have caused a deselection (as it was called due to
|
|||
// selection being lost) so we're ok to discard `deselected` here.
|
|||
var deselectedItems = operation.DeselectedItems ?? |
|||
SelectedItems<T>.Create(deselected, deselectedSource); |
|||
|
|||
var e = new SelectionModelSelectionChangedEventArgs<T>( |
|||
SelectedIndexes<T>.Create(deselected), |
|||
SelectedIndexes<T>.Create(selected), |
|||
deselectedItems, |
|||
SelectedItems<T>.Create(selected, ItemsView)); |
|||
SelectionChanged?.Invoke(this, e); |
|||
_untypedSelectionChanged?.Invoke(this, e); |
|||
} |
|||
} |
|||
|
|||
if (oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
indexesChanged = true; |
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
RaisePropertyChanged(nameof(SelectedItem)); |
|||
} |
|||
|
|||
if (oldAnchorIndex != _anchorIndex) |
|||
{ |
|||
indexesChanged = true; |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
|
|||
if (indexesChanged) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedIndexes)); |
|||
RaisePropertyChanged(nameof(SelectedItems)); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_operation = null; |
|||
} |
|||
} |
|||
|
|||
public struct BatchUpdateOperation : IDisposable |
|||
{ |
|||
private readonly SelectionModel<T> _owner; |
|||
private bool _isDisposed; |
|||
|
|||
public BatchUpdateOperation(SelectionModel<T> owner) |
|||
{ |
|||
_owner = owner; |
|||
_isDisposed = false; |
|||
owner.BeginBatchUpdate(); |
|||
} |
|||
|
|||
internal Operation Operation => _owner._operation!; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!_isDisposed) |
|||
{ |
|||
_owner?.EndBatchUpdate(); |
|||
_isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class Operation |
|||
{ |
|||
public Operation(SelectionModel<T> owner) |
|||
{ |
|||
AnchorIndex = owner.AnchorIndex; |
|||
SelectedIndex = owner.SelectedIndex; |
|||
} |
|||
|
|||
public int UpdateCount { get; set; } |
|||
public bool IsSourceUpdate { get; set; } |
|||
public bool SkipLostSelection { get; set; } |
|||
public int AnchorIndex { get; set; } |
|||
public int SelectedIndex { get; set; } |
|||
public List<IndexRange>? SelectedRanges { get; set; } |
|||
public List<IndexRange>? DeselectedRanges { get; set; } |
|||
public IReadOnlyList<T>? DeselectedItems { get; set; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public class SelectionModelIndexesChangedEventArgs : EventArgs |
|||
{ |
|||
public SelectionModelIndexesChangedEventArgs(int startIndex, int delta) |
|||
{ |
|||
StartIndex = startIndex; |
|||
Delta = delta; |
|||
} |
|||
|
|||
public int StartIndex { get; } |
|||
public int Delta { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Selection; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public abstract class SelectionModelSelectionChangedEventArgs : EventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public abstract IReadOnlyList<int> DeselectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public abstract IReadOnlyList<int> SelectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> DeselectedItems => GetUntypedDeselectedItems(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> SelectedItems => GetUntypedSelectedItems(); |
|||
|
|||
protected abstract IReadOnlyList<object?> GetUntypedDeselectedItems(); |
|||
protected abstract IReadOnlyList<object?> GetUntypedSelectedItems(); |
|||
} |
|||
|
|||
public class SelectionModelSelectionChangedEventArgs<T> : SelectionModelSelectionChangedEventArgs |
|||
{ |
|||
private IReadOnlyList<object?>? _deselectedItems; |
|||
private IReadOnlyList<object?>? _selectedItems; |
|||
|
|||
public SelectionModelSelectionChangedEventArgs( |
|||
IReadOnlyList<int>? deselectedIndices = null, |
|||
IReadOnlyList<int>? selectedIndices = null, |
|||
IReadOnlyList<T>? deselectedItems = null, |
|||
IReadOnlyList<T>? selectedItems = null) |
|||
{ |
|||
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>(); |
|||
SelectedIndexes = selectedIndices ?? Array.Empty<int>(); |
|||
DeselectedItems = deselectedItems ?? Array.Empty<T>(); |
|||
SelectedItems = selectedItems ?? Array.Empty<T>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public override IReadOnlyList<int> DeselectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public override IReadOnlyList<int> SelectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public new IReadOnlyList<T> DeselectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public new IReadOnlyList<T> SelectedItems { get; } |
|||
|
|||
protected override IReadOnlyList<object?> GetUntypedDeselectedItems() |
|||
{ |
|||
return _deselectedItems ??= (DeselectedItems as IReadOnlyList<object?>) ?? |
|||
new SelectedItems<T>.Untyped(DeselectedItems); |
|||
} |
|||
|
|||
protected override IReadOnlyList<object?> GetUntypedSelectedItems() |
|||
{ |
|||
return _selectedItems ??= (SelectedItems as IReadOnlyList<object?>) ?? |
|||
new SelectedItems<T>.Untyped(SelectedItems); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,286 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public abstract class SelectionNodeBase<T> : ICollectionChangedListener |
|||
{ |
|||
private IEnumerable? _source; |
|||
private bool _rangesEnabled; |
|||
private List<IndexRange>? _ranges; |
|||
private int _collectionChanging; |
|||
|
|||
protected IEnumerable? Source |
|||
{ |
|||
get => _source; |
|||
set |
|||
{ |
|||
if (_source != value) |
|||
{ |
|||
ItemsView?.RemoveListener(this); |
|||
_source = value; |
|||
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null; |
|||
ItemsView?.AddListener(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected bool IsSourceCollectionChanging => _collectionChanging > 0; |
|||
|
|||
protected bool RangesEnabled |
|||
{ |
|||
get => _rangesEnabled; |
|||
set |
|||
{ |
|||
if (_rangesEnabled != value) |
|||
{ |
|||
_rangesEnabled = value; |
|||
|
|||
if (!_rangesEnabled) |
|||
{ |
|||
_ranges = null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal ItemsSourceView<T>? ItemsView { get; set; } |
|||
|
|||
internal IReadOnlyList<IndexRange> Ranges |
|||
{ |
|||
get |
|||
{ |
|||
if (!RangesEnabled) |
|||
{ |
|||
throw new InvalidOperationException("Ranges not enabled."); |
|||
} |
|||
|
|||
return _ranges ??= new List<IndexRange>(); |
|||
} |
|||
} |
|||
|
|||
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
++_collectionChanging; |
|||
} |
|||
|
|||
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
OnSourceCollectionChanged(e); |
|||
} |
|||
|
|||
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (--_collectionChanging == 0) |
|||
{ |
|||
OnSourceCollectionChangeFinished(); |
|||
} |
|||
} |
|||
|
|||
protected abstract void OnSourceCollectionChangeFinished(); |
|||
|
|||
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta); |
|||
|
|||
private protected abstract void OnSourceReset(); |
|||
|
|||
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems); |
|||
|
|||
private protected int CommitSelect(IndexRange range) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Add(_ranges, range); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Add(_ranges, ranges); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitDeselect(IndexRange range) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Remove(_ranges, range); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges) |
|||
{ |
|||
if (RangesEnabled && _ranges is object) |
|||
{ |
|||
return IndexRange.Remove(_ranges, ranges); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var shifted = false; |
|||
|
|||
if (_ranges is object) |
|||
{ |
|||
List<IndexRange>? toAdd = null; |
|||
|
|||
for (var i = 0; i < Ranges!.Count; ++i) |
|||
{ |
|||
var range = Ranges[i]; |
|||
|
|||
// The range is after the inserted items, need to shift the range right
|
|||
if (range.End >= index) |
|||
{ |
|||
int begin = range.Begin; |
|||
|
|||
// If the index left of newIndex is inside the range,
|
|||
// Split the range and remember the left piece to add later
|
|||
if (range.Contains(index - 1)) |
|||
{ |
|||
range.Split(index - 1, out var before, out _); |
|||
(toAdd ??= new List<IndexRange>()).Add(before); |
|||
begin = index; |
|||
} |
|||
|
|||
// Shift the range to the right
|
|||
_ranges[i] = new IndexRange(begin + count, range.End + count); |
|||
shifted = true; |
|||
} |
|||
} |
|||
|
|||
if (toAdd is object) |
|||
{ |
|||
foreach (var range in toAdd) |
|||
{ |
|||
IndexRange.Add(_ranges, range); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? count : 0, |
|||
}; |
|||
} |
|||
|
|||
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var removedRange = new IndexRange(index, index + count - 1); |
|||
bool shifted = false; |
|||
List<T>? removed = null; |
|||
|
|||
if (_ranges is object) |
|||
{ |
|||
var deselected = new List<IndexRange>(); |
|||
|
|||
if (IndexRange.Remove(_ranges, removedRange, deselected) > 0) |
|||
{ |
|||
removed = new List<T>(); |
|||
|
|||
foreach (var range in deselected) |
|||
{ |
|||
for (var i = range.Begin; i <= range.End; ++i) |
|||
{ |
|||
#pragma warning disable CS8604
|
|||
removed.Add((T)items[i - index]); |
|||
#pragma warning restore CS8604
|
|||
} |
|||
} |
|||
} |
|||
|
|||
for (var i = 0; i < Ranges!.Count; ++i) |
|||
{ |
|||
var existing = Ranges[i]; |
|||
|
|||
if (existing.End > removedRange.Begin) |
|||
{ |
|||
_ranges[i] = new IndexRange(existing.Begin - count, existing.End - count); |
|||
shifted = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? -count : 0, |
|||
RemovedItems = removed, |
|||
}; |
|||
} |
|||
|
|||
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
var shiftDelta = 0; |
|||
var shiftIndex = -1; |
|||
List<T>? removed = null; |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems); |
|||
shiftIndex = change.ShiftIndex; |
|||
shiftDelta = change.ShiftDelta; |
|||
break; |
|||
} |
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems); |
|||
shiftIndex = change.ShiftIndex; |
|||
shiftDelta = change.ShiftDelta; |
|||
removed = change.RemovedItems; |
|||
break; |
|||
} |
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems); |
|||
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems); |
|||
shiftIndex = removeChange.ShiftIndex; |
|||
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; |
|||
removed = removeChange.RemovedItems; |
|||
} |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
OnSourceReset(); |
|||
break; |
|||
} |
|||
|
|||
if (shiftDelta != 0) |
|||
{ |
|||
OnIndexesChanged(shiftIndex, shiftDelta); |
|||
} |
|||
|
|||
if (removed is object) |
|||
{ |
|||
OnSelectionChanged(removed); |
|||
} |
|||
} |
|||
|
|||
private protected struct CollectionChangeState |
|||
{ |
|||
public int ShiftIndex; |
|||
public int ShiftDelta; |
|||
public List<T>? RemovedItems; |
|||
} |
|||
} |
|||
} |
|||
@ -1,894 +0,0 @@ |
|||
// 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.ComponentModel; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModel : ISelectionModel, IDisposable |
|||
{ |
|||
private readonly SelectionNode _rootNode; |
|||
private bool _singleSelect; |
|||
private bool _autoSelect; |
|||
private int _operationCount; |
|||
private IndexPath _oldAnchorIndex; |
|||
private IReadOnlyList<IndexPath>? _selectedIndicesCached; |
|||
private IReadOnlyList<object?>? _selectedItemsCached; |
|||
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs; |
|||
|
|||
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested; |
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged; |
|||
|
|||
public SelectionModel() |
|||
{ |
|||
_rootNode = new SelectionNode(this, null); |
|||
SharedLeafNode = new SelectionNode(this, null); |
|||
} |
|||
|
|||
public object? Source |
|||
{ |
|||
get => _rootNode.Source; |
|||
set |
|||
{ |
|||
if (_rootNode.Source != value) |
|||
{ |
|||
var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0; |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
// Temporarily prevent auto-select when switching source.
|
|||
var restoreAutoSelect = _autoSelect; |
|||
_autoSelect = false; |
|||
|
|||
try |
|||
{ |
|||
using (var operation = new Operation(this)) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_autoSelect = restoreAutoSelect; |
|||
} |
|||
} |
|||
|
|||
_rootNode.Source = value; |
|||
ApplyAutoSelect(true); |
|||
|
|||
RaisePropertyChanged("Source"); |
|||
|
|||
if (raiseChanged) |
|||
{ |
|||
var e = new SelectionModelSelectionChangedEventArgs( |
|||
null, |
|||
SelectedIndices, |
|||
null, |
|||
SelectedItems); |
|||
OnSelectionChanged(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool SingleSelect |
|||
{ |
|||
get => _singleSelect; |
|||
set |
|||
{ |
|||
if (_singleSelect != value) |
|||
{ |
|||
_singleSelect = value; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (value && selectedIndices != null && selectedIndices.Count > 0) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
|
|||
// We want to be single select, so make sure there is only
|
|||
// one selected item.
|
|||
var firstSelectionIndexPath = selectedIndices[0]; |
|||
ClearSelection(resetAnchor: true); |
|||
SelectWithPathImpl(firstSelectionIndexPath, select: true); |
|||
SelectedIndex = firstSelectionIndexPath; |
|||
} |
|||
|
|||
RaisePropertyChanged("SingleSelect"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool RetainSelectionOnReset |
|||
{ |
|||
get => _rootNode.RetainSelectionOnReset; |
|||
set => _rootNode.RetainSelectionOnReset = value; |
|||
} |
|||
|
|||
public bool AutoSelect |
|||
{ |
|||
get => _autoSelect; |
|||
set |
|||
{ |
|||
if (_autoSelect != value) |
|||
{ |
|||
_autoSelect = value; |
|||
ApplyAutoSelect(true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IndexPath AnchorIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath anchor = default; |
|||
|
|||
if (_rootNode.AnchorIndex >= 0) |
|||
{ |
|||
var path = new List<int>(); |
|||
SelectionNode? current = _rootNode; |
|||
|
|||
while (current?.AnchorIndex >= 0) |
|||
{ |
|||
path.Add(current.AnchorIndex); |
|||
current = current.GetAt(current.AnchorIndex, false, default); |
|||
} |
|||
|
|||
anchor = new IndexPath(path); |
|||
} |
|||
|
|||
return anchor; |
|||
} |
|||
set |
|||
{ |
|||
var oldValue = AnchorIndex; |
|||
|
|||
if (value != null) |
|||
{ |
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
value, |
|||
realizeChildren: true, |
|||
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth)); |
|||
} |
|||
else |
|||
{ |
|||
_rootNode.AnchorIndex = -1; |
|||
} |
|||
|
|||
if (_operationCount == 0 && oldValue != AnchorIndex) |
|||
{ |
|||
RaisePropertyChanged("AnchorIndex"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IndexPath SelectedIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath selectedIndex = default; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (selectedIndices?.Count > 0) |
|||
{ |
|||
selectedIndex = selectedIndices[0]; |
|||
} |
|||
|
|||
return selectedIndex; |
|||
} |
|||
set |
|||
{ |
|||
if (!IsSelectedAt(value) || SelectedItems.Count > 1) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
ClearSelection(resetAnchor: true); |
|||
SelectWithPathImpl(value, select: true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public object? SelectedItem |
|||
{ |
|||
get |
|||
{ |
|||
object? item = null; |
|||
var selectedItems = SelectedItems; |
|||
|
|||
if (selectedItems?.Count > 0) |
|||
{ |
|||
item = selectedItems[0]; |
|||
} |
|||
|
|||
return item; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<object?> SelectedItems |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedItemsCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
var count = 0; |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
count += currentInfo.Node.SelectedCount; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected items,
|
|||
// we create a custom IReadOnlyList implementation that calls back using a delegate to find
|
|||
// the selected item at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of objects.
|
|||
var selectedItems = new SelectedItems<object?, SelectedItemInfo> ( |
|||
selectedInfos, |
|||
count, |
|||
(infos, index) => |
|||
{ |
|||
var currentIndex = 0; |
|||
object? item = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
var targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
item = node.ItemsSourceView!.GetAt(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedItems property was read."); |
|||
} |
|||
} |
|||
|
|||
return item; |
|||
}); |
|||
|
|||
_selectedItemsCached = selectedItems; |
|||
} |
|||
|
|||
return _selectedItemsCached; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<IndexPath> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedIndicesCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
var count = 0; |
|||
|
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
count += currentInfo.Node.SelectedCount; |
|||
} |
|||
}); |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected indices,
|
|||
// we create a custom VectorView implimentation that calls back using a delegate to find
|
|||
// the IndexPath at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of IndexPaths.
|
|||
var indices = new SelectedItems<IndexPath, SelectedItemInfo>( |
|||
selectedInfos, |
|||
count, |
|||
(infos, index) => // callback for GetAt(index)
|
|||
{ |
|||
var currentIndex = 0; |
|||
IndexPath path = default; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
path = info.Path.CloneWithChildIndex(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedIndices property was read."); |
|||
} |
|||
} |
|||
|
|||
return path; |
|||
}); |
|||
|
|||
_selectedIndicesCached = indices; |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
internal SelectionNode SharedLeafNode { get; private set; } |
|||
|
|||
public void Dispose() |
|||
{ |
|||
ClearSelection(resetAnchor: false); |
|||
_rootNode.Cleanup(); |
|||
_rootNode.Dispose(); |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
} |
|||
|
|||
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index); |
|||
|
|||
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index); |
|||
|
|||
public void Select(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(index, select: true); |
|||
} |
|||
|
|||
public void Select(int groupIndex, int itemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithGroupImpl(groupIndex, itemIndex, select: true); |
|||
} |
|||
|
|||
public void SelectAt(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithPathImpl(index, select: true); |
|||
} |
|||
|
|||
public void Deselect(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(index, select: false); |
|||
} |
|||
|
|||
public void Deselect(int groupIndex, int itemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithGroupImpl(groupIndex, itemIndex, select: false); |
|||
} |
|||
|
|||
public void DeselectAt(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithPathImpl(index, select: false); |
|||
} |
|||
|
|||
public bool IsSelected(int index) => _rootNode.IsSelected(index); |
|||
|
|||
public bool IsSelected(int grouIndex, int itemIndex) |
|||
{ |
|||
return IsSelectedAt(new IndexPath(grouIndex, itemIndex)); |
|||
} |
|||
|
|||
public bool IsSelectedAt(IndexPath index) |
|||
{ |
|||
var path = index; |
|||
SelectionNode? node = _rootNode; |
|||
|
|||
for (int i = 0; i < path.GetSize() - 1; i++) |
|||
{ |
|||
var childIndex = path.GetAt(i); |
|||
node = node.GetAt(childIndex, false, default); |
|||
|
|||
if (node == null) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return node.IsSelected(index.GetAt(index.GetSize() - 1)); |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartial(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
throw new ArgumentException("Index must be >= 0", nameof(index)); |
|||
} |
|||
|
|||
var isSelected = _rootNode.IsSelectedWithPartial(index); |
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartial(int groupIndex, int itemIndex) |
|||
{ |
|||
if (groupIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex)); |
|||
} |
|||
|
|||
if (itemIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex)); |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
var childNode = _rootNode.GetAt(groupIndex, false, default); |
|||
|
|||
if (childNode != null) |
|||
{ |
|||
isSelected = childNode.IsSelectedWithPartial(itemIndex); |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartialAt(IndexPath index) |
|||
{ |
|||
var path = index; |
|||
var isRealized = true; |
|||
SelectionNode? node = _rootNode; |
|||
|
|||
for (int i = 0; i < path.GetSize() - 1; i++) |
|||
{ |
|||
var childIndex = path.GetAt(i); |
|||
node = node.GetAt(childIndex, false, default); |
|||
|
|||
if (node == null) |
|||
{ |
|||
isRealized = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
|
|||
if (isRealized) |
|||
{ |
|||
var size = path.GetSize(); |
|||
if (size == 0) |
|||
{ |
|||
isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes()); |
|||
} |
|||
else |
|||
{ |
|||
isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1)); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorImpl(index, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(AnchorIndex, index, select: true); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorImpl(index, select: false); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(AnchorIndex, index, select: false); |
|||
} |
|||
|
|||
public void SelectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(start, end, select: true); |
|||
} |
|||
|
|||
public void DeselectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(start, end, select: false); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
using var operation = new Operation(this); |
|||
|
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: true, |
|||
info => |
|||
{ |
|||
if (info.Node.DataCount > 0) |
|||
{ |
|||
info.Node.SelectAll(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void ClearSelection() |
|||
{ |
|||
using var operation = new Operation(this); |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
public IDisposable Update() => new Operation(this); |
|||
|
|||
protected void OnPropertyChanged(string propertyName) |
|||
{ |
|||
RaisePropertyChanged(propertyName); |
|||
} |
|||
|
|||
private void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
|
|||
public void OnSelectionInvalidatedDueToCollectionChange( |
|||
bool selectionInvalidated, |
|||
IReadOnlyList<object?>? removedItems) |
|||
{ |
|||
SelectionModelSelectionChangedEventArgs? e = null; |
|||
|
|||
if (selectionInvalidated) |
|||
{ |
|||
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null); |
|||
} |
|||
|
|||
OnSelectionChanged(e); |
|||
ApplyAutoSelect(true); |
|||
} |
|||
|
|||
internal IObservable<object?>? ResolvePath( |
|||
object data, |
|||
IndexPath dataIndexPath, |
|||
IndexPath finalIndexPath) |
|||
{ |
|||
IObservable<object?>? resolved = null; |
|||
|
|||
// Raise ChildrenRequested event if there is a handler
|
|||
if (ChildrenRequested != null) |
|||
{ |
|||
if (_childrenRequestedEventArgs == null) |
|||
{ |
|||
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs( |
|||
data, |
|||
dataIndexPath, |
|||
finalIndexPath, |
|||
false); |
|||
} |
|||
else |
|||
{ |
|||
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false); |
|||
} |
|||
|
|||
ChildrenRequested(this, _childrenRequestedEventArgs); |
|||
resolved = _childrenRequestedEventArgs.Children; |
|||
|
|||
// Clear out the values in the args so that it cannot be used after the event handler call.
|
|||
_childrenRequestedEventArgs.Initialize(null, default, default, true); |
|||
} |
|||
|
|||
return resolved; |
|||
} |
|||
|
|||
private void ClearSelection(bool resetAnchor) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
info => info.Node.Clear()); |
|||
|
|||
if (resetAnchor) |
|||
{ |
|||
AnchorIndex = default; |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) |
|||
{ |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
|
|||
if (e != null) |
|||
{ |
|||
SelectionChanged?.Invoke(this, e); |
|||
|
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
RaisePropertyChanged(nameof(SelectedIndices)); |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedItem)); |
|||
RaisePropertyChanged(nameof(SelectedItems)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SelectImpl(int index, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
var selected = _rootNode.Select(index, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(index); |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex)); |
|||
var selected = childNode!.Select(itemIndex, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(groupIndex, itemIndex); |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectWithPathImpl(IndexPath index, bool select) |
|||
{ |
|||
bool selected = false; |
|||
|
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
index, |
|||
true, |
|||
(currentNode, path, depth, childIndex) => |
|||
{ |
|||
if (depth == path.GetSize() - 1) |
|||
{ |
|||
selected = currentNode.Select(childIndex, select); |
|||
} |
|||
} |
|||
); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = index; |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorImpl(int index, bool select) |
|||
{ |
|||
int anchorIndex = 0; |
|||
var anchor = AnchorIndex; |
|||
|
|||
if (anchor != null) |
|||
{ |
|||
anchorIndex = anchor.GetAt(0); |
|||
} |
|||
|
|||
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select); |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) |
|||
{ |
|||
var startGroupIndex = 0; |
|||
var startItemIndex = 0; |
|||
var anchorIndex = AnchorIndex; |
|||
|
|||
if (anchorIndex != null) |
|||
{ |
|||
startGroupIndex = anchorIndex.GetAt(0); |
|||
startItemIndex = anchorIndex.GetAt(1); |
|||
} |
|||
|
|||
// Make sure start > end
|
|||
if (startGroupIndex > endGroupIndex || |
|||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex)) |
|||
{ |
|||
int temp = startGroupIndex; |
|||
startGroupIndex = endGroupIndex; |
|||
endGroupIndex = temp; |
|||
temp = startItemIndex; |
|||
startItemIndex = endItemIndex; |
|||
endItemIndex = temp; |
|||
} |
|||
|
|||
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) |
|||
{ |
|||
var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!; |
|||
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; |
|||
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; |
|||
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) |
|||
{ |
|||
var winrtStart = start; |
|||
var winrtEnd = end; |
|||
|
|||
// Make sure start <= end
|
|||
if (winrtEnd.CompareTo(winrtStart) == -1) |
|||
{ |
|||
var temp = winrtStart; |
|||
winrtStart = winrtEnd; |
|||
winrtEnd = temp; |
|||
} |
|||
|
|||
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
|
|||
SelectionTreeHelper.TraverseRangeRealizeChildren( |
|||
_rootNode, |
|||
winrtStart, |
|||
winrtEnd, |
|||
info => |
|||
{ |
|||
if (info.Path >= winrtStart && info.Path <= winrtEnd) |
|||
{ |
|||
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); |
|||
} |
|||
}); |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void BeginOperation() |
|||
{ |
|||
if (_operationCount++ == 0) |
|||
{ |
|||
_oldAnchorIndex = AnchorIndex; |
|||
_rootNode.BeginOperation(); |
|||
} |
|||
} |
|||
|
|||
private void EndOperation() |
|||
{ |
|||
if (_operationCount == 0) |
|||
{ |
|||
throw new AvaloniaInternalException("No selection operation in progress."); |
|||
} |
|||
|
|||
SelectionModelSelectionChangedEventArgs? e = null; |
|||
|
|||
if (--_operationCount == 0) |
|||
{ |
|||
ApplyAutoSelect(false); |
|||
|
|||
var changes = new List<SelectionNodeOperation>(); |
|||
_rootNode.EndOperation(changes); |
|||
|
|||
if (changes.Count > 0) |
|||
{ |
|||
var changeSet = new SelectionModelChangeSet(changes); |
|||
e = changeSet.CreateEventArgs(); |
|||
} |
|||
|
|||
OnSelectionChanged(e); |
|||
|
|||
if (_oldAnchorIndex != AnchorIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
|
|||
_rootNode.Cleanup(); |
|||
_oldAnchorIndex = default; |
|||
} |
|||
} |
|||
|
|||
private void ApplyAutoSelect(bool createOperation) |
|||
{ |
|||
if (AutoSelect) |
|||
{ |
|||
_selectedIndicesCached = null; |
|||
|
|||
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) |
|||
{ |
|||
if (createOperation) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(0, true); |
|||
} |
|||
else |
|||
{ |
|||
SelectImpl(0, true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class SelectedItemInfo : ISelectedItemInfo |
|||
{ |
|||
public SelectedItemInfo(SelectionNode node, IndexPath path) |
|||
{ |
|||
Node = node; |
|||
Path = path; |
|||
} |
|||
|
|||
public SelectionNode Node { get; } |
|||
public IndexPath Path { get; } |
|||
public int Count => Node.SelectedCount; |
|||
} |
|||
|
|||
private struct Operation : IDisposable |
|||
{ |
|||
private readonly SelectionModel _manager; |
|||
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation(); |
|||
public void Dispose() => _manager.EndOperation(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,170 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class SelectionModelChangeSet |
|||
{ |
|||
private readonly List<SelectionNodeOperation> _changes; |
|||
|
|||
public SelectionModelChangeSet(List<SelectionNodeOperation> changes) |
|||
{ |
|||
_changes = changes; |
|||
} |
|||
|
|||
public SelectionModelSelectionChangedEventArgs CreateEventArgs() |
|||
{ |
|||
var deselectedIndexCount = 0; |
|||
var selectedIndexCount = 0; |
|||
var deselectedItemCount = 0; |
|||
var selectedItemCount = 0; |
|||
|
|||
foreach (var change in _changes) |
|||
{ |
|||
deselectedIndexCount += change.DeselectedCount; |
|||
selectedIndexCount += change.SelectedCount; |
|||
|
|||
if (change.Items != null) |
|||
{ |
|||
deselectedItemCount += change.DeselectedCount; |
|||
selectedItemCount += change.SelectedCount; |
|||
} |
|||
} |
|||
|
|||
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( |
|||
_changes, |
|||
deselectedIndexCount, |
|||
GetDeselectedIndexAt); |
|||
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( |
|||
_changes, |
|||
selectedIndexCount, |
|||
GetSelectedIndexAt); |
|||
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>( |
|||
_changes, |
|||
deselectedItemCount, |
|||
GetDeselectedItemAt); |
|||
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>( |
|||
_changes, |
|||
selectedItemCount, |
|||
GetSelectedItemAt); |
|||
|
|||
return new SelectionModelSelectionChangedEventArgs( |
|||
deselectedIndices, |
|||
selectedIndices, |
|||
deselectedItems, |
|||
selectedItems); |
|||
} |
|||
|
|||
private IndexPath GetDeselectedIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; |
|||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private IndexPath GetSelectedIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.SelectedCount; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges; |
|||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private object? GetDeselectedItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; |
|||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private object? GetSelectedItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges; |
|||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private IndexPath GetIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index, |
|||
Func<SelectionNodeOperation, int> getCount, |
|||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges) |
|||
{ |
|||
var currentIndex = 0; |
|||
IndexPath path = default; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var currentCount = getCount(info); |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); |
|||
path = info.Path.CloneWithChildIndex(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
|
|||
return path; |
|||
} |
|||
|
|||
private object? GetItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index, |
|||
Func<SelectionNodeOperation, int> getCount, |
|||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges) |
|||
{ |
|||
var currentIndex = 0; |
|||
object? item = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var currentCount = getCount(info); |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); |
|||
item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null; |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
|
|||
return item; |
|||
} |
|||
|
|||
private int GetIndexAt(List<IndexRange>? ranges, int index) |
|||
{ |
|||
var currentIndex = 0; |
|||
|
|||
if (ranges != null) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
var currentCount = (range.End - range.Begin) + 1; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
return range.Begin + (index - currentIndex); |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
} |
|||
|
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
// 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; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
|
|||
/// </summary>
|
|||
public class SelectionModelChildrenRequestedEventArgs : EventArgs |
|||
{ |
|||
private object? _source; |
|||
private IndexPath _sourceIndexPath; |
|||
private IndexPath _finalIndexPath; |
|||
private bool _throwOnAccess; |
|||
|
|||
internal SelectionModelChildrenRequestedEventArgs( |
|||
object source, |
|||
IndexPath sourceIndexPath, |
|||
IndexPath finalIndexPath, |
|||
bool throwOnAccess) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
|
|||
/// object.
|
|||
/// </summary>
|
|||
public IObservable<object?>? Children { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the object whose children are being requested.
|
|||
/// </summary>
|
|||
public object Source |
|||
{ |
|||
get |
|||
{ |
|||
if (_throwOnAccess) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); |
|||
} |
|||
|
|||
return _source!; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the object whose children are being requested.
|
|||
/// </summary>
|
|||
public IndexPath SourceIndex |
|||
{ |
|||
get |
|||
{ |
|||
if (_throwOnAccess) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); |
|||
} |
|||
|
|||
return _sourceIndexPath; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the final object which is being attempted to be retrieved.
|
|||
/// </summary>
|
|||
public IndexPath FinalIndex |
|||
{ |
|||
get |
|||
{ |
|||
if (_throwOnAccess) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); |
|||
} |
|||
|
|||
return _finalIndexPath; |
|||
} |
|||
} |
|||
|
|||
internal void Initialize( |
|||
object? source, |
|||
IndexPath sourceIndexPath, |
|||
IndexPath finalIndexPath, |
|||
bool throwOnAccess) |
|||
{ |
|||
if (!throwOnAccess && source == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(source)); |
|||
} |
|||
|
|||
_source = source; |
|||
_sourceIndexPath = sourceIndexPath; |
|||
_finalIndexPath = finalIndexPath; |
|||
_throwOnAccess = throwOnAccess; |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
// 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; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModelSelectionChangedEventArgs : EventArgs |
|||
{ |
|||
public SelectionModelSelectionChangedEventArgs( |
|||
IReadOnlyList<IndexPath>? deselectedIndices, |
|||
IReadOnlyList<IndexPath>? selectedIndices, |
|||
IReadOnlyList<object?>? deselectedItems, |
|||
IReadOnlyList<object?>? selectedItems) |
|||
{ |
|||
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>(); |
|||
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>(); |
|||
DeselectedItems = deselectedItems ?? Array.Empty<object?>(); |
|||
SelectedItems= selectedItems ?? Array.Empty<object?>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the indices of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<IndexPath> DeselectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indices of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<IndexPath> SelectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> DeselectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> SelectedItems { get; } |
|||
} |
|||
} |
|||
@ -1,971 +0,0 @@ |
|||
// 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; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Tracks nested selection.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// SelectionNode is the internal tree data structure that we keep track of for selection in
|
|||
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
|
|||
/// collection changes and keeps the selected indices up to date. This can either be a leaf
|
|||
/// node or a non leaf node.
|
|||
/// </remarks>
|
|||
internal class SelectionNode : IDisposable |
|||
{ |
|||
private readonly SelectionModel _manager; |
|||
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>(); |
|||
private readonly SelectionNode? _parent; |
|||
private readonly List<IndexRange> _selected = new List<IndexRange>(); |
|||
private readonly List<int> _selectedIndicesCached = new List<int>(); |
|||
private IDisposable? _childrenSubscription; |
|||
private SelectionNodeOperation? _operation; |
|||
private object? _source; |
|||
private bool _selectedIndicesCacheIsValid; |
|||
private bool _retainSelectionOnReset; |
|||
private List<object?>? _selectedItems; |
|||
|
|||
public SelectionNode(SelectionModel manager, SelectionNode? parent) |
|||
{ |
|||
_manager = manager; |
|||
_parent = parent; |
|||
} |
|||
|
|||
public int AnchorIndex { get; set; } = -1; |
|||
|
|||
public bool RetainSelectionOnReset |
|||
{ |
|||
get => _retainSelectionOnReset; |
|||
set |
|||
{ |
|||
if (_retainSelectionOnReset != value) |
|||
{ |
|||
_retainSelectionOnReset = value; |
|||
|
|||
if (_retainSelectionOnReset) |
|||
{ |
|||
_selectedItems = new List<object?>(); |
|||
PopulateSelectedItemsFromSelectedIndices(); |
|||
} |
|||
else |
|||
{ |
|||
_selectedItems = null; |
|||
} |
|||
|
|||
foreach (var child in _childrenNodes) |
|||
{ |
|||
if (child != null) |
|||
{ |
|||
child.RetainSelectionOnReset = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public object? Source |
|||
{ |
|||
get => _source; |
|||
set |
|||
{ |
|||
if (_source != value) |
|||
{ |
|||
if (_source != null) |
|||
{ |
|||
ClearSelection(); |
|||
ClearChildNodes(); |
|||
UnhookCollectionChangedHandler(); |
|||
} |
|||
|
|||
_source = value; |
|||
|
|||
// Setup ItemsSourceView
|
|||
var newDataSource = value as ItemsSourceView; |
|||
|
|||
if (value != null && newDataSource == null) |
|||
{ |
|||
newDataSource = new ItemsSourceView((IEnumerable)value); |
|||
} |
|||
|
|||
ItemsSourceView = newDataSource; |
|||
|
|||
TrimInvalidSelections(); |
|||
PopulateSelectedItemsFromSelectedIndices(); |
|||
HookupCollectionChangedHandler(); |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void TrimInvalidSelections() |
|||
{ |
|||
if (_selected == null || ItemsSourceView == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); |
|||
var removed = new List<IndexRange>(); |
|||
var removedCount = IndexRange.Intersect(_selected, validRange, removed); |
|||
|
|||
if (removedCount > 0) |
|||
{ |
|||
using var operation = _manager.Update(); |
|||
SelectedCount -= removedCount; |
|||
OnSelectionChanged(); |
|||
_operation!.Deselected(removed); |
|||
} |
|||
} |
|||
|
|||
public ItemsSourceView? ItemsSourceView { get; private set; } |
|||
public int DataCount => ItemsSourceView?.Count ?? 0; |
|||
public int ChildrenNodeCount => _childrenNodes.Count; |
|||
public int RealizedChildrenNodeCount { get; private set; } |
|||
|
|||
public IndexPath IndexPath |
|||
{ |
|||
get |
|||
{ |
|||
var path = new List<int>(); ; |
|||
var parent = _parent; |
|||
var child = this; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var childNodes = parent._childrenNodes; |
|||
var index = childNodes.IndexOf(child); |
|||
|
|||
// We are walking up to the parent, so the path will be backwards
|
|||
path.Insert(0, index); |
|||
child = parent; |
|||
parent = parent._parent; |
|||
} |
|||
|
|||
return new IndexPath(path); |
|||
} |
|||
} |
|||
|
|||
// For a genuine tree view, we dont know which node is leaf until we
|
|||
// actually walk to it, so currently the tree builds up to the leaf. I don't
|
|||
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
|
|||
// an explosion of node objects. However, I'm still creating the m_childrenNodes
|
|||
// collection unfortunately.
|
|||
public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) |
|||
{ |
|||
SelectionNode? child = null; |
|||
|
|||
if (realizeChild) |
|||
{ |
|||
if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count) |
|||
{ |
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
|
|||
if (_childrenNodes.Count == 0) |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
for (int i = 0; i < ItemsSourceView.Count; i++) |
|||
{ |
|||
_childrenNodes.Add(null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (_childrenNodes[index] == null) |
|||
{ |
|||
var childData = ItemsSourceView!.GetAt(index); |
|||
IObservable<object?>? resolver = null; |
|||
|
|||
if (childData != null) |
|||
{ |
|||
var childDataIndexPath = IndexPath.CloneWithChildIndex(index); |
|||
resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); |
|||
} |
|||
|
|||
if (resolver != null) |
|||
{ |
|||
child = new SelectionNode(_manager, parent: this); |
|||
child.SetChildrenObservable(resolver); |
|||
} |
|||
else if (childData is IEnumerable<object> || childData is IList) |
|||
{ |
|||
child = new SelectionNode(_manager, parent: this); |
|||
child.Source = childData; |
|||
} |
|||
else |
|||
{ |
|||
child = _manager.SharedLeafNode; |
|||
} |
|||
|
|||
if (_operation != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.BeginOperation(); |
|||
} |
|||
|
|||
_childrenNodes[index] = child; |
|||
RealizedChildrenNodeCount++; |
|||
} |
|||
else |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
|
|||
return child; |
|||
} |
|||
|
|||
public void SetChildrenObservable(IObservable<object?> resolver) |
|||
{ |
|||
_childrenSubscription = resolver.Subscribe(x => |
|||
{ |
|||
if (Source != null) |
|||
{ |
|||
using (_manager.Update()) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
this, |
|||
realizeChildren: false, |
|||
info => info.Node.Clear()); |
|||
} |
|||
} |
|||
|
|||
Source = x; |
|||
}); |
|||
} |
|||
|
|||
public int SelectedCount { get; private set; } |
|||
|
|||
public bool IsSelected(int index) |
|||
{ |
|||
var isSelected = false; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
if (range.Contains(index)) |
|||
{ |
|||
isSelected = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial() |
|||
{ |
|||
var isSelected = (bool?)false; |
|||
|
|||
if (_parent != null) |
|||
{ |
|||
var parentsChildren = _parent._childrenNodes; |
|||
|
|||
var myIndexInParent = parentsChildren.IndexOf(this); |
|||
|
|||
if (myIndexInParent != -1) |
|||
{ |
|||
isSelected = _parent.IsSelectedWithPartial(myIndexInParent); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial(int index) |
|||
{ |
|||
SelectionState selectionState; |
|||
|
|||
if (_childrenNodes.Count == 0 || // no nodes realized
|
|||
_childrenNodes.Count <= index || // target node is not realized
|
|||
_childrenNodes[index] == null || // target node is not realized
|
|||
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
|
|||
{ |
|||
// Ask parent if the target node is selected.
|
|||
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// targetNode is the node representing the index. This node is the parent.
|
|||
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
|
|||
// based on children of targetNode.
|
|||
var targetNode = _childrenNodes[index]; |
|||
selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes(); |
|||
} |
|||
|
|||
return ConvertToNullableBool(selectionState); |
|||
} |
|||
|
|||
public int SelectedIndex |
|||
{ |
|||
get => SelectedCount > 0 ? SelectedIndices[0] : -1; |
|||
set |
|||
{ |
|||
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value))) |
|||
{ |
|||
ClearSelection(); |
|||
|
|||
if (value != -1) |
|||
{ |
|||
Select(value, true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public List<int> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (!_selectedIndicesCacheIsValid) |
|||
{ |
|||
_selectedIndicesCacheIsValid = true; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
for (int index = range.Begin; index <= range.End; index++) |
|||
{ |
|||
// Avoid duplicates
|
|||
if (!_selectedIndicesCached.Contains(index)) |
|||
{ |
|||
_selectedIndicesCached.Add(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Sort the list for easy consumption
|
|||
_selectedIndicesCached.Sort(); |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<object> SelectedItems |
|||
{ |
|||
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x)); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_childrenSubscription?.Dispose(); |
|||
ItemsSourceView?.Dispose(); |
|||
ClearChildNodes(); |
|||
UnhookCollectionChangedHandler(); |
|||
} |
|||
|
|||
public void BeginOperation() |
|||
{ |
|||
if (_operation != null) |
|||
{ |
|||
throw new AvaloniaInternalException("Selection operation already in progress."); |
|||
} |
|||
|
|||
_operation = new SelectionNodeOperation(this); |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.BeginOperation(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void EndOperation(List<SelectionNodeOperation> changes) |
|||
{ |
|||
if (_operation == null) |
|||
{ |
|||
throw new AvaloniaInternalException("No selection operation in progress."); |
|||
} |
|||
|
|||
if (_operation.HasChanges) |
|||
{ |
|||
changes.Add(_operation); |
|||
} |
|||
|
|||
_operation = null; |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.EndOperation(changes); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool Cleanup() |
|||
{ |
|||
var result = SelectedCount == 0; |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null) |
|||
{ |
|||
if (child.Cleanup()) |
|||
{ |
|||
child.Dispose(); |
|||
_childrenNodes[i] = null; |
|||
} |
|||
else |
|||
{ |
|||
result = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public bool Select(int index, bool select) |
|||
{ |
|||
return Select(index, select, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
public bool ToggleSelect(int index) |
|||
{ |
|||
return Select(index, !IsSelected(index)); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
var size = ItemsSourceView.Count; |
|||
|
|||
if (size > 0) |
|||
{ |
|||
SelectRange(new IndexRange(0, size - 1), select: true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Clear() => ClearSelection(); |
|||
|
|||
public bool SelectRange(IndexRange range, bool select) |
|||
{ |
|||
if (IsValidIndex(range.Begin) && IsValidIndex(range.End)) |
|||
{ |
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void HookupCollectionChangedHandler() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
ItemsSourceView.CollectionChanged += OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private void UnhookCollectionChangedHandler() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
ItemsSourceView.CollectionChanged -= OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private bool IsValidIndex(int index) |
|||
{ |
|||
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count); |
|||
} |
|||
|
|||
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
var selected = new List<IndexRange>(); |
|||
|
|||
SelectedCount += IndexRange.Add(_selected, addRange, selected); |
|||
|
|||
if (selected.Count > 0) |
|||
{ |
|||
_operation?.Selected(selected); |
|||
|
|||
if (_selectedItems != null && ItemsSourceView != null) |
|||
{ |
|||
for (var i = addRange.Begin; i <= addRange.End; ++i) |
|||
{ |
|||
_selectedItems.Add(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
var removed = new List<IndexRange>(); |
|||
|
|||
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed); |
|||
|
|||
if (removed.Count > 0) |
|||
{ |
|||
_operation?.Deselected(removed); |
|||
|
|||
if (_selectedItems != null) |
|||
{ |
|||
for (var i = removeRange.Begin; i <= removeRange.End; ++i) |
|||
{ |
|||
_selectedItems.Remove(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void ClearSelection() |
|||
{ |
|||
// Deselect all items
|
|||
if (_selected.Count > 0) |
|||
{ |
|||
_operation?.Deselected(_selected); |
|||
_selected.Clear(); |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
_selectedItems?.Clear(); |
|||
SelectedCount = 0; |
|||
AnchorIndex = -1; |
|||
} |
|||
|
|||
private void ClearChildNodes() |
|||
{ |
|||
for (int i = 0; i < _childrenNodes.Count; i++) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.Dispose(); |
|||
_childrenNodes[i] = null; |
|||
} |
|||
} |
|||
|
|||
RealizedChildrenNodeCount = 0; |
|||
} |
|||
|
|||
private bool Select(int index, bool select, bool raiseOnSelectionChanged) |
|||
{ |
|||
if (IsValidIndex(index)) |
|||
{ |
|||
// Ignore duplicate selection calls
|
|||
if (IsSelected(index) == select) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var range = new IndexRange(index, index); |
|||
|
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
bool selectionInvalidated = false; |
|||
List<object?>? removed = null; |
|||
|
|||
switch (args.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
{ |
|||
if (_selectedItems == null) |
|||
{ |
|||
ClearSelection(); |
|||
} |
|||
else |
|||
{ |
|||
removed = RecreateSelectionFromSelectedItems(); |
|||
} |
|||
|
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); |
|||
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionInvalidated) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed); |
|||
} |
|||
|
|||
private bool OnItemsAdded(int index, int count) |
|||
{ |
|||
var selectionInvalidated = false; |
|||
|
|||
// Update ranges for leaf items
|
|||
var toAdd = new List<IndexRange>(); |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[i]; |
|||
|
|||
// The range is after the inserted items, need to shift the range right
|
|||
if (range.End >= index) |
|||
{ |
|||
int begin = range.Begin; |
|||
|
|||
// If the index left of newIndex is inside the range,
|
|||
// Split the range and remember the left piece to add later
|
|||
if (range.Contains(index - 1)) |
|||
{ |
|||
range.Split(index - 1, out var before, out _); |
|||
toAdd.Add(before); |
|||
begin = index; |
|||
} |
|||
|
|||
// Shift the range to the right
|
|||
_selected[i] = new IndexRange(begin + count, range.End + count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Add the left sides of the split ranges
|
|||
_selected.AddRange(toAdd); |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
_childrenNodes.Insert(index, null); |
|||
} |
|||
} |
|||
|
|||
// Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex += count; |
|||
} |
|||
|
|||
// Check if adding a node invalidated an ancestors
|
|||
// selection state. For example if parent was selected before
|
|||
// adding a new item makes the parent partially selected now.
|
|||
if (!selectionInvalidated) |
|||
{ |
|||
var parent = _parent; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var isSelected = parent.IsSelectedWithPartial(); |
|||
|
|||
// If a parent is selected, then it will become partially selected.
|
|||
// If it is not selected or partially selected - there is no change.
|
|||
if (isSelected == true) |
|||
{ |
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
parent = parent._parent; |
|||
} |
|||
} |
|||
|
|||
return selectionInvalidated; |
|||
} |
|||
|
|||
private (bool, List<object?>) OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var selectionInvalidated = false; |
|||
var removed = new List<object?>(); |
|||
var count = items.Count; |
|||
var isSelected = false; |
|||
|
|||
for (int i = 0; i <= count - 1; i++) |
|||
{ |
|||
if (IsSelected(index + i)) |
|||
{ |
|||
isSelected = true; |
|||
removed.Add(items[i]); |
|||
} |
|||
} |
|||
|
|||
if (isSelected) |
|||
{ |
|||
var removeRange = new IndexRange(index, index + count - 1); |
|||
SelectedCount -= IndexRange.Remove(_selected, removeRange); |
|||
selectionInvalidated = true; |
|||
|
|||
if (_selectedItems != null) |
|||
{ |
|||
foreach (var i in items) |
|||
{ |
|||
_selectedItems.Remove(i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[i]; |
|||
|
|||
// The range is after the removed items, need to shift the range left
|
|||
if (range.End > index) |
|||
{ |
|||
// Shift the range to the left
|
|||
_selected[i] = new IndexRange(range.Begin - count, range.End - count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
if (_childrenNodes[index] != null) |
|||
{ |
|||
removed.AddRange(_childrenNodes[index]!.SelectedItems); |
|||
RealizedChildrenNodeCount--; |
|||
_childrenNodes[index]!.Dispose(); |
|||
} |
|||
_childrenNodes.RemoveAt(index); |
|||
} |
|||
} |
|||
|
|||
//Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex -= count; |
|||
} |
|||
|
|||
return (selectionInvalidated, removed); |
|||
} |
|||
|
|||
private void OnSelectionChanged() |
|||
{ |
|||
_selectedIndicesCacheIsValid = false; |
|||
_selectedIndicesCached.Clear(); |
|||
} |
|||
|
|||
public static bool? ConvertToNullableBool(SelectionState isSelected) |
|||
{ |
|||
bool? result = null; // PartialySelected
|
|||
|
|||
if (isSelected == SelectionState.Selected) |
|||
{ |
|||
result = true; |
|||
} |
|||
else if (isSelected == SelectionState.NotSelected) |
|||
{ |
|||
result = false; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes() |
|||
{ |
|||
var selectionState = SelectionState.NotSelected; |
|||
int realizedChildrenNodeCount = RealizedChildrenNodeCount; |
|||
int selectedCount = SelectedCount; |
|||
|
|||
if (realizedChildrenNodeCount != 0 || selectedCount != 0) |
|||
{ |
|||
// There are realized children or some selected leaves.
|
|||
int dataCount = DataCount; |
|||
if (realizedChildrenNodeCount == 0 && selectedCount > 0) |
|||
{ |
|||
// All nodes are leaves under it - we didn't create children nodes as an optimization.
|
|||
// See if all/some or none of the leaves are selected.
|
|||
selectionState = dataCount != selectedCount ? |
|||
SelectionState.PartiallySelected : |
|||
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// There are child nodes, walk them individually and evaluate based on each child
|
|||
// being selected/not selected or partially selected.
|
|||
selectedCount = 0; |
|||
int notSelectedCount = 0; |
|||
for (int i = 0; i < ChildrenNodeCount; i++) |
|||
{ |
|||
var child = GetAt(i, false, default); |
|||
|
|||
if (child != null) |
|||
{ |
|||
// child is realized, ask it.
|
|||
var isChildSelected = IsSelectedWithPartial(i); |
|||
if (isChildSelected == null) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
else if (isChildSelected == true) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// not realized.
|
|||
if (IsSelected(i)) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
|
|||
if (selectedCount > 0 && notSelectedCount > 0) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionState != SelectionState.PartiallySelected) |
|||
{ |
|||
if (selectedCount != 0 && selectedCount != dataCount) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
} |
|||
else |
|||
{ |
|||
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return selectionState; |
|||
} |
|||
|
|||
private void PopulateSelectedItemsFromSelectedIndices() |
|||
{ |
|||
if (_selectedItems != null) |
|||
{ |
|||
_selectedItems.Clear(); |
|||
|
|||
foreach (var i in SelectedIndices) |
|||
{ |
|||
_selectedItems.Add(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private List<object?> RecreateSelectionFromSelectedItems() |
|||
{ |
|||
var removed = new List<object?>(); |
|||
|
|||
_selected.Clear(); |
|||
SelectedCount = 0; |
|||
|
|||
for (var i = 0; i < _selectedItems!.Count; ++i) |
|||
{ |
|||
var item = _selectedItems[i]; |
|||
var index = ItemsSourceView!.IndexOf(item); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
IndexRange.Add(_selected, new IndexRange(index, index)); |
|||
++SelectedCount; |
|||
} |
|||
else |
|||
{ |
|||
removed.Add(item); |
|||
_selectedItems.RemoveAt(i--); |
|||
} |
|||
} |
|||
|
|||
return removed; |
|||
} |
|||
|
|||
public enum SelectionState |
|||
{ |
|||
Selected, |
|||
NotSelected, |
|||
PartiallySelected |
|||
} |
|||
} |
|||
} |
|||
@ -1,110 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class SelectionNodeOperation : ISelectedItemInfo |
|||
{ |
|||
private readonly SelectionNode _owner; |
|||
private List<IndexRange>? _selected; |
|||
private List<IndexRange>? _deselected; |
|||
private int _selectedCount = -1; |
|||
private int _deselectedCount = -1; |
|||
|
|||
public SelectionNodeOperation(SelectionNode owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0; |
|||
public List<IndexRange>? SelectedRanges => _selected; |
|||
public List<IndexRange>? DeselectedRanges => _deselected; |
|||
public IndexPath Path => _owner.IndexPath; |
|||
public ItemsSourceView? Items => _owner.ItemsSourceView; |
|||
|
|||
public int SelectedCount |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedCount == -1) |
|||
{ |
|||
_selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0; |
|||
} |
|||
|
|||
return _selectedCount; |
|||
} |
|||
} |
|||
|
|||
public int DeselectedCount |
|||
{ |
|||
get |
|||
{ |
|||
if (_deselectedCount == -1) |
|||
{ |
|||
_deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0; |
|||
} |
|||
|
|||
return _deselectedCount; |
|||
} |
|||
} |
|||
|
|||
public void Selected(IndexRange range) |
|||
{ |
|||
Add(range, ref _selected, _deselected); |
|||
_selectedCount = -1; |
|||
} |
|||
|
|||
public void Selected(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
Selected(range); |
|||
} |
|||
} |
|||
|
|||
public void Deselected(IndexRange range) |
|||
{ |
|||
Add(range, ref _deselected, _selected); |
|||
_deselectedCount = -1; |
|||
} |
|||
|
|||
public void Deselected(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
Deselected(range); |
|||
} |
|||
} |
|||
|
|||
private static void Add( |
|||
IndexRange range, |
|||
ref List<IndexRange>? add, |
|||
List<IndexRange>? remove) |
|||
{ |
|||
if (remove != null) |
|||
{ |
|||
var removed = new List<IndexRange>(); |
|||
IndexRange.Remove(remove, range, removed); |
|||
var selected = IndexRange.Subtract(range, removed); |
|||
|
|||
if (selected.Any()) |
|||
{ |
|||
add ??= new List<IndexRange>(); |
|||
|
|||
foreach (var r in selected) |
|||
{ |
|||
IndexRange.Add(add, r); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
add ??= new List<IndexRange>(); |
|||
IndexRange.Add(add, range); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Utilities; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
internal interface ICollectionChangedListener |
|||
{ |
|||
void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
} |
|||
|
|||
internal class CollectionChangedEventManager : IWeakSubscriber<NotifyCollectionChangedEventArgs> |
|||
{ |
|||
public static CollectionChangedEventManager Instance { get; } = new CollectionChangedEventManager(); |
|||
|
|||
private ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>> _entries = |
|||
new ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>>(); |
|||
|
|||
private CollectionChangedEventManager() |
|||
{ |
|||
} |
|||
|
|||
public void AddListener(INotifyCollectionChanged collection, ICollectionChangedListener listener) |
|||
{ |
|||
collection = collection ?? throw new ArgumentNullException(nameof(collection)); |
|||
listener = listener ?? throw new ArgumentNullException(nameof(listener)); |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
if (!_entries.TryGetValue(collection, out var listeners)) |
|||
{ |
|||
listeners = new List<WeakReference<ICollectionChangedListener>>(); |
|||
_entries.Add(collection, listeners); |
|||
WeakSubscriptionManager.Subscribe( |
|||
collection, |
|||
nameof(INotifyCollectionChanged.CollectionChanged), |
|||
this); |
|||
} |
|||
|
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target) && target == listener) |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Collection listener already added for this collection/listener combination."); |
|||
} |
|||
} |
|||
|
|||
listeners.Add(new WeakReference<ICollectionChangedListener>(listener)); |
|||
} |
|||
|
|||
public void RemoveListener(INotifyCollectionChanged collection, ICollectionChangedListener listener) |
|||
{ |
|||
collection = collection ?? throw new ArgumentNullException(nameof(collection)); |
|||
listener = listener ?? throw new ArgumentNullException(nameof(listener)); |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
if (_entries.TryGetValue(collection, out var listeners)) |
|||
{ |
|||
for (var i = 0; i < listeners.Count; ++i) |
|||
{ |
|||
if (listeners[i].TryGetTarget(out var target) && target == listener) |
|||
{ |
|||
listeners.RemoveAt(i); |
|||
|
|||
if (listeners.Count == 0) |
|||
{ |
|||
WeakSubscriptionManager.Unsubscribe( |
|||
collection, |
|||
nameof(INotifyCollectionChanged.CollectionChanged), |
|||
this); |
|||
_entries.Remove(collection); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException( |
|||
"Collection listener not registered for this collection/listener combination."); |
|||
} |
|||
|
|||
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
static void Notify( |
|||
INotifyCollectionChanged incc, |
|||
NotifyCollectionChangedEventArgs args, |
|||
List<WeakReference<ICollectionChangedListener>> listeners) |
|||
{ |
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.PreChanged(incc, args); |
|||
} |
|||
} |
|||
|
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.Changed(incc, args); |
|||
} |
|||
} |
|||
|
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.PostChanged(incc, args); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (sender is INotifyCollectionChanged incc && _entries.TryGetValue(incc, out var listeners)) |
|||
{ |
|||
var l = listeners.ToList(); |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Notify(incc, e, l); |
|||
} |
|||
else |
|||
{ |
|||
var inccCapture = incc; |
|||
var eCapture = e; |
|||
Dispatcher.UIThread.Post(() => Notify(inccCapture, eCapture, l)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,189 +0,0 @@ |
|||
// 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; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
internal static class SelectionTreeHelper |
|||
{ |
|||
public static void TraverseIndexPath( |
|||
SelectionNode root, |
|||
IndexPath path, |
|||
bool realizeChildren, |
|||
Action<SelectionNode, IndexPath, int, int> nodeAction) |
|||
{ |
|||
var node = root; |
|||
|
|||
for (int depth = 0; depth < path.GetSize(); depth++) |
|||
{ |
|||
int childIndex = path.GetAt(depth); |
|||
nodeAction(node, path, depth, childIndex); |
|||
|
|||
if (depth < path.GetSize() - 1) |
|||
{ |
|||
node = node.GetAt(childIndex, realizeChildren, path)!; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static void Traverse( |
|||
SelectionNode root, |
|||
bool realizeChildren, |
|||
Action<TreeWalkNodeInfo> nodeAction) |
|||
{ |
|||
var pendingNodes = new List<TreeWalkNodeInfo>(); |
|||
var current = new IndexPath(null); |
|||
|
|||
pendingNodes.Add(new TreeWalkNodeInfo(root, current)); |
|||
|
|||
while (pendingNodes.Count > 0) |
|||
{ |
|||
var nextNode = pendingNodes.Last(); |
|||
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
|||
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; |
|||
for (int i = count - 1; i >= 0; i--) |
|||
{ |
|||
var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path); |
|||
var childPath = nextNode.Path.CloneWithChildIndex(i); |
|||
if (child != null) |
|||
{ |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node)); |
|||
} |
|||
} |
|||
|
|||
// Queue the children first and then perform the action. This way
|
|||
// the action can remove the children in the action if necessary
|
|||
nodeAction(nextNode); |
|||
} |
|||
} |
|||
|
|||
public static void TraverseRangeRealizeChildren( |
|||
SelectionNode root, |
|||
IndexPath start, |
|||
IndexPath end, |
|||
Action<TreeWalkNodeInfo> nodeAction) |
|||
{ |
|||
var pendingNodes = new List<TreeWalkNodeInfo>(); |
|||
var current = start; |
|||
|
|||
// Build up the stack to account for the depth first walk up to the
|
|||
// start index path.
|
|||
TraverseIndexPath( |
|||
root, |
|||
start, |
|||
true, |
|||
(node, path, depth, childIndex) => |
|||
{ |
|||
var currentPath = StartPath(path, depth); |
|||
bool isStartPath = IsSubSet(start, currentPath); |
|||
bool isEndPath = IsSubSet(end, currentPath); |
|||
|
|||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
|||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1; |
|||
|
|||
for (int i = endIndex; i >= startIndex; i--) |
|||
{ |
|||
var child = node.GetAt(i, true, end); |
|||
if (child != null) |
|||
{ |
|||
var childPath = currentPath.CloneWithChildIndex(i); |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node)); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// From the start index path, do a depth first walk as long as the
|
|||
// current path is less than the end path.
|
|||
while (pendingNodes.Count > 0) |
|||
{ |
|||
var info = pendingNodes.Last(); |
|||
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
|||
int depth = info.Path.GetSize(); |
|||
bool isStartPath = IsSubSet(start, info.Path); |
|||
bool isEndPath = IsSubSet(end, info.Path); |
|||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
|||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; |
|||
for (int i = endIndex; i >= startIndex; i--) |
|||
{ |
|||
var child = info.Node.GetAt(i, true, end); |
|||
if (child != null) |
|||
{ |
|||
var childPath = info.Path.CloneWithChildIndex(i); |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node)); |
|||
} |
|||
} |
|||
|
|||
nodeAction(info); |
|||
|
|||
if (info.Path.CompareTo(end) == 0) |
|||
{ |
|||
// We reached the end index path. stop iterating.
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static bool IsSubSet(IndexPath path, IndexPath subset) |
|||
{ |
|||
var subsetSize = subset.GetSize(); |
|||
if (path.GetSize() < subsetSize) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
for (int i = 0; i < subsetSize; i++) |
|||
{ |
|||
if (path.GetAt(i) != subset.GetAt(i)) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private static IndexPath StartPath(IndexPath path, int length) |
|||
{ |
|||
var subPath = new List<int>(); |
|||
for (int i = 0; i < length; i++) |
|||
{ |
|||
subPath.Add(path.GetAt(i)); |
|||
} |
|||
|
|||
return new IndexPath(subPath); |
|||
} |
|||
|
|||
public struct TreeWalkNodeInfo |
|||
{ |
|||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent) |
|||
{ |
|||
node = node ?? throw new ArgumentNullException(nameof(node)); |
|||
|
|||
Node = node; |
|||
Path = indexPath; |
|||
ParentNode = parent; |
|||
} |
|||
|
|||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath) |
|||
{ |
|||
node = node ?? throw new ArgumentNullException(nameof(node)); |
|||
|
|||
Node = node; |
|||
Path = indexPath; |
|||
ParentNode = null; |
|||
} |
|||
|
|||
public SelectionNode Node { get; } |
|||
public IndexPath Path { get; } |
|||
public SelectionNode? ParentNode { get; } |
|||
}; |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
Compat issues with assembly Avalonia.Markup.Xaml: |
|||
MembersMustExist : Member 'public Avalonia.Data.Binding Avalonia.Markup.Xaml.Templates.TreeDataTemplate.ItemsSource.get()' does not exist in the implementation but it does exist in the contract. |
|||
MembersMustExist : Member 'public void Avalonia.Markup.Xaml.Templates.TreeDataTemplate.ItemsSource.set(Avalonia.Data.Binding)' does not exist in the implementation but it does exist in the contract. |
|||
Total Issues: 2 |
|||
@ -1,95 +0,0 @@ |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class IndexPathTests |
|||
{ |
|||
[Fact] |
|||
public void Simple_Index() |
|||
{ |
|||
var a = new IndexPath(1); |
|||
|
|||
Assert.Equal(1, a.GetSize()); |
|||
Assert.Equal(1, a.GetAt(0)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Equal_Paths() |
|||
{ |
|||
var a = new IndexPath(1); |
|||
var b = new IndexPath(1); |
|||
|
|||
Assert.True(a == b); |
|||
Assert.False(a != b); |
|||
Assert.True(a.Equals(b)); |
|||
Assert.Equal(0, a.CompareTo(b)); |
|||
Assert.Equal(a.GetHashCode(), b.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Unequal_Paths() |
|||
{ |
|||
var a = new IndexPath(1); |
|||
var b = new IndexPath(2); |
|||
|
|||
Assert.False(a == b); |
|||
Assert.True(a != b); |
|||
Assert.False(a.Equals(b)); |
|||
Assert.Equal(-1, a.CompareTo(b)); |
|||
Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Equal_Null_Path() |
|||
{ |
|||
var a = new IndexPath(null); |
|||
var b = new IndexPath(null); |
|||
|
|||
Assert.True(a == b); |
|||
Assert.False(a != b); |
|||
Assert.True(a.Equals(b)); |
|||
Assert.Equal(0, a.CompareTo(b)); |
|||
Assert.Equal(a.GetHashCode(), b.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Unequal_Null_Path() |
|||
{ |
|||
var a = new IndexPath(null); |
|||
var b = new IndexPath(2); |
|||
|
|||
Assert.False(a == b); |
|||
Assert.True(a != b); |
|||
Assert.False(a.Equals(b)); |
|||
Assert.Equal(-1, a.CompareTo(b)); |
|||
Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Default_Is_Null_Path() |
|||
{ |
|||
var a = new IndexPath(null); |
|||
var b = default(IndexPath); |
|||
|
|||
Assert.True(a == b); |
|||
Assert.False(a != b); |
|||
Assert.True(a.Equals(b)); |
|||
Assert.Equal(0, a.CompareTo(b)); |
|||
Assert.Equal(a.GetHashCode(), b.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Null_Equality() |
|||
{ |
|||
var a = new IndexPath(null); |
|||
var b = new IndexPath(1); |
|||
|
|||
// Implementing operator == on a struct automatically implements an operator which
|
|||
// accepts null, so make sure this does something useful.
|
|||
Assert.True(a == null); |
|||
Assert.False(a != null); |
|||
Assert.False(b == null); |
|||
Assert.True(b != null); |
|||
} |
|||
} |
|||
} |
|||
@ -1,389 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class IndexRangeTests |
|||
{ |
|||
[Fact] |
|||
public void Add_Should_Add_Range_To_Empty_List() |
|||
{ |
|||
var ranges = new List<IndexRange>(); |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); |
|||
|
|||
Assert.Equal(5, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 4) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Non_Intersecting_Range_At_End() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 4) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Non_Intersecting_Range_At_Beginning() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); |
|||
|
|||
Assert.Equal(5, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 4) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Non_Intersecting_Range_In_Middle() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 4), new IndexRange(14, 16) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10), new IndexRange(14, 16) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Intersecting_Range_Start() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(6, 9), selected); |
|||
|
|||
Assert.Equal(2, result); |
|||
Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(6, 7) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Intersecting_Range_End() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(9, 12), selected); |
|||
|
|||
Assert.Equal(2, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 12) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(11, 12) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Add_Intersecting_Range_Both() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(6, 12), selected); |
|||
|
|||
Assert.Equal(4, result); |
|||
Assert.Equal(new[] { new IndexRange(6, 12) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 12) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Join_Two_Intersecting_Ranges() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(8, 14), selected); |
|||
|
|||
Assert.Equal(1, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 14) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(11, 11) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Join_Two_Intersecting_Ranges_And_Add_Ranges() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(6, 18), selected); |
|||
|
|||
Assert.Equal(7, result); |
|||
Assert.Equal(new[] { new IndexRange(6, 18) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 11), new IndexRange(15, 18) }, selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Add_Should_Not_Add_Already_Selected_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var selected = new List<IndexRange>(); |
|||
var result = IndexRange.Add(ranges, new IndexRange(9, 10), selected); |
|||
|
|||
Assert.Equal(0, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); |
|||
Assert.Empty(selected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Items_From_Beginning() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 10) }; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); |
|||
|
|||
Assert.Equal(2, result); |
|||
Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 1) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Items_From_End() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 10) }; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); |
|||
|
|||
Assert.Equal(2, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(9, 10) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Entire_Range_Start() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) }; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); |
|||
|
|||
Assert.Equal(6, result); |
|||
Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 5) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Entire_Range_End() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) }; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); |
|||
|
|||
Assert.Equal(6, result); |
|||
Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(5, 10) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Entire_Range_Start_End() |
|||
{ |
|||
var ranges = new List<IndexRange> |
|||
{ |
|||
new IndexRange(0, 2), |
|||
new IndexRange(3, 7), |
|||
new IndexRange(8, 10) |
|||
}; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); |
|||
|
|||
Assert.Equal(6, result); |
|||
Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() |
|||
{ |
|||
var ranges = new List<IndexRange> |
|||
{ |
|||
new IndexRange(0, 2), |
|||
new IndexRange(3, 7), |
|||
new IndexRange(8, 10) |
|||
}; |
|||
var removed = new List<IndexRange>(); |
|||
var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); |
|||
|
|||
Assert.Equal(8, result); |
|||
Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Entire_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Empty(ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Start_Of_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 12) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Equal(new[] { new IndexRange(11, 12) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_End_Of_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 12) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(10, 12), deselected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Overlapping_End_Of_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 12) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(10, 14), deselected); |
|||
|
|||
Assert.Equal(3, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Middle_Of_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(10, 20) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(12, 16), deselected); |
|||
|
|||
Assert.Equal(5, result); |
|||
Assert.Equal(new[] { new IndexRange(10, 11), new IndexRange(17, 20) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(12, 16) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Multiple_Ranges() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(6, 15), deselected); |
|||
|
|||
Assert.Equal(6, result); |
|||
Assert.Equal(new[] { new IndexRange(16, 18) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 14) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_1() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(9, 15), deselected); |
|||
|
|||
Assert.Equal(5, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(16, 18) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 14) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_2() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(8, 13), deselected); |
|||
|
|||
Assert.Equal(5, result); |
|||
Assert.Equal(new[] { new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 13) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_3() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(9, 13), deselected); |
|||
|
|||
Assert.Equal(4, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); |
|||
Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 13) }, deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Remove_Should_Do_Nothing_For_Unselected_Range() |
|||
{ |
|||
var ranges = new List<IndexRange> { new IndexRange(8, 10) }; |
|||
var deselected = new List<IndexRange>(); |
|||
var result = IndexRange.Remove(ranges, new IndexRange(2, 4), deselected); |
|||
|
|||
Assert.Equal(0, result); |
|||
Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); |
|||
Assert.Empty(deselected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Stress_Test() |
|||
{ |
|||
const int iterations = 100; |
|||
var random = new Random(0); |
|||
var selection = new List<IndexRange>(); |
|||
var expected = new List<int>(); |
|||
|
|||
IndexRange Generate() |
|||
{ |
|||
var start = random.Next(100); |
|||
return new IndexRange(start, start + random.Next(20)); |
|||
} |
|||
|
|||
for (var i = 0; i < iterations; ++i) |
|||
{ |
|||
var toAdd = random.Next(5); |
|||
|
|||
for (var j = 0; j < toAdd; ++j) |
|||
{ |
|||
var range = Generate(); |
|||
IndexRange.Add(selection, range); |
|||
|
|||
for (var k = range.Begin; k <= range.End; ++k) |
|||
{ |
|||
if (!expected.Contains(k)) |
|||
{ |
|||
expected.Add(k); |
|||
} |
|||
} |
|||
|
|||
var actual = IndexRange.EnumerateIndices(selection).ToList(); |
|||
expected.Sort(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
var toRemove = random.Next(5); |
|||
|
|||
for (var j = 0; j < toRemove; ++j) |
|||
{ |
|||
var range = Generate(); |
|||
IndexRange.Remove(selection, range); |
|||
|
|||
for (var k = range.Begin; k <= range.End; ++k) |
|||
{ |
|||
expected.Remove(k); |
|||
} |
|||
|
|||
var actual = IndexRange.EnumerateIndices(selection).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
selection.Clear(); |
|||
expected.Clear(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue