committed by
GitHub
223 changed files with 8561 additions and 8328 deletions
@ -0,0 +1,113 @@ |
|||
#include "common.h" |
|||
|
|||
@interface CocoaThemeObserver : NSObject |
|||
-(id)initWithCallback:(IAvnActionCallback *)callback; |
|||
@end |
|||
|
|||
class PlatformSettings : public ComSingleObject<IAvnPlatformSettings, &IID_IAvnPlatformSettings> |
|||
{ |
|||
CocoaThemeObserver* observer; |
|||
|
|||
public: |
|||
FORWARD_IUNKNOWN() |
|||
virtual AvnPlatformThemeVariant GetPlatformTheme() override |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
if (@available(macOS 10.14, *)) |
|||
{ |
|||
if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua |
|||
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) { |
|||
return AvnPlatformThemeVariant::Light; |
|||
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua |
|||
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark) { |
|||
return AvnPlatformThemeVariant::Dark; |
|||
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua |
|||
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { |
|||
return AvnPlatformThemeVariant::HighContrastLight; |
|||
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua |
|||
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { |
|||
return AvnPlatformThemeVariant::HighContrastDark; |
|||
} |
|||
} |
|||
return AvnPlatformThemeVariant::Light; |
|||
} |
|||
} |
|||
|
|||
virtual unsigned int GetAccentColor() override |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
if (@available(macOS 10.14, *)) |
|||
{ |
|||
auto color = [NSColor controlAccentColor]; |
|||
return to_argb(color); |
|||
} |
|||
else |
|||
{ |
|||
return 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
virtual void RegisterColorsChange(IAvnActionCallback *callback) override |
|||
{ |
|||
if (@available(macOS 10.14, *)) |
|||
{ |
|||
observer = [[CocoaThemeObserver alloc] initWithCallback: callback]; |
|||
[[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil]; |
|||
} |
|||
} |
|||
|
|||
private: |
|||
unsigned int to_argb(NSColor* color) |
|||
{ |
|||
const CGFloat* components = CGColorGetComponents(color.CGColor); |
|||
unsigned int alpha = static_cast<unsigned int>(CGColorGetAlpha(color.CGColor) * 0xFF); |
|||
unsigned int red = static_cast<unsigned int>(components[0] * 0xFF); |
|||
unsigned int green = static_cast<unsigned int>(components[1] * 0xFF); |
|||
unsigned int blue = static_cast<unsigned int>(components[2] * 0xFF); |
|||
return (alpha << 24) + (red << 16) + (green << 8) + blue; |
|||
} |
|||
}; |
|||
|
|||
@implementation CocoaThemeObserver |
|||
{ |
|||
ComPtr<IAvnActionCallback> _callback; |
|||
} |
|||
- (id) initWithCallback:(IAvnActionCallback *)callback{ |
|||
self = [super init]; |
|||
if (self) { |
|||
_callback = callback; |
|||
} |
|||
return self; |
|||
} |
|||
|
|||
/*- (void)didChangeValueForKey:(NSString *)key { |
|||
if([key isEqualToString:@"effectiveAppearance"]) { |
|||
_callback->Run(); |
|||
} |
|||
else { |
|||
[super didChangeValueForKey:key]; |
|||
} |
|||
}*/ |
|||
|
|||
- (void)observeValueForKeyPath:(NSString *)keyPath |
|||
ofObject:(id)object |
|||
change:(NSDictionary *)change |
|||
context:(void *)context { |
|||
if([keyPath isEqualToString:@"effectiveAppearance"]) { |
|||
_callback->Run(); |
|||
} else { |
|||
[super observeValueForKeyPath:keyPath |
|||
ofObject:object |
|||
change:change |
|||
context:context]; |
|||
} |
|||
} |
|||
@end |
|||
|
|||
extern IAvnPlatformSettings* CreatePlatformSettings() |
|||
{ |
|||
return new PlatformSettings(); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
public interface IActivityNavigationService |
|||
{ |
|||
event EventHandler<AndroidBackRequestedEventArgs> BackRequested; |
|||
} |
|||
|
|||
public class AndroidBackRequestedEventArgs : EventArgs |
|||
{ |
|||
public bool Handled { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
using System; |
|||
using Android; |
|||
using Android.Content; |
|||
using Android.Content.Res; |
|||
using Android.Graphics; |
|||
using Android.Provider; |
|||
using Android.Views.Accessibility; |
|||
using AndroidX.Core.Content.Resources; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Color = Avalonia.Media.Color; |
|||
|
|||
namespace Avalonia.Android.Platform; |
|||
|
|||
// TODO: ideally should be created per view/activity.
|
|||
internal class AndroidPlatformSettings : DefaultPlatformSettings |
|||
{ |
|||
private PlatformColorValues _latestValues; |
|||
|
|||
public AndroidPlatformSettings() |
|||
{ |
|||
_latestValues = base.GetColorValues(); |
|||
} |
|||
|
|||
public override PlatformColorValues GetColorValues() |
|||
{ |
|||
return _latestValues; |
|||
} |
|||
|
|||
internal void OnViewConfigurationChanged(Context context) |
|||
{ |
|||
if (context.Resources?.Configuration is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var systemTheme = (context.Resources.Configuration.UiMode & UiMode.NightMask) switch |
|||
{ |
|||
UiMode.NightYes => PlatformThemeVariant.Dark, |
|||
UiMode.NightNo => PlatformThemeVariant.Light, |
|||
_ => throw new ArgumentOutOfRangeException() |
|||
}; |
|||
|
|||
if (OperatingSystem.IsAndroidVersionAtLeast(31)) |
|||
{ |
|||
// See https://developer.android.com/reference/android/R.color
|
|||
var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
|
|||
var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
|
|||
var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
|
|||
|
|||
_latestValues = new PlatformColorValues |
|||
{ |
|||
ThemeVariant = systemTheme, |
|||
ContrastPreference = IsHighContrast(context), |
|||
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B), |
|||
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B), |
|||
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B), |
|||
}; |
|||
} |
|||
else if (OperatingSystem.IsAndroidVersionAtLeast(23)) |
|||
{ |
|||
// See https://developer.android.com/reference/android/R.attr
|
|||
var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent
|
|||
var accent = array.GetColor(0, 0); |
|||
|
|||
_latestValues = new PlatformColorValues |
|||
{ |
|||
ThemeVariant = systemTheme, |
|||
ContrastPreference = IsHighContrast(context), |
|||
AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B) |
|||
}; |
|||
array.Recycle(); |
|||
} |
|||
else |
|||
{ |
|||
_latestValues = _latestValues with { ThemeVariant = systemTheme }; |
|||
} |
|||
|
|||
OnColorValuesChanged(_latestValues); |
|||
} |
|||
|
|||
private static ColorContrastPreference IsHighContrast(Context context) |
|||
{ |
|||
try |
|||
{ |
|||
return Settings.Secure.GetInt(context.ContentResolver, "high_text_contrast_enabled", 0) == 1 |
|||
? ColorContrastPreference.High : ColorContrastPreference.NoPreference; |
|||
} |
|||
catch |
|||
{ |
|||
return ColorContrastPreference.NoPreference; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Android.Platform |
|||
{ |
|||
internal class AndroidSystemNavigationManager : ISystemNavigationManager |
|||
{ |
|||
public event EventHandler<RoutedEventArgs> BackRequested; |
|||
|
|||
public AndroidSystemNavigationManager(IActivityNavigationService? navigationService) |
|||
{ |
|||
if(navigationService != null) |
|||
{ |
|||
navigationService.BackRequested += OnBackRequested; |
|||
} |
|||
} |
|||
|
|||
private void OnBackRequested(object sender, AndroidBackRequestedEventArgs e) |
|||
{ |
|||
var routedEventArgs = new RoutedEventArgs(); |
|||
|
|||
BackRequested?.Invoke(this, routedEventArgs); |
|||
|
|||
e.Handled = routedEventArgs.Handled; |
|||
} |
|||
} |
|||
} |
|||
@ -1,275 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
public readonly struct CharacterBufferRange : IReadOnlyList<char> |
|||
{ |
|||
/// <summary>
|
|||
/// Getting an empty character string
|
|||
/// </summary>
|
|||
public static CharacterBufferRange Empty => new CharacterBufferRange(); |
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from character array
|
|||
/// </summary>
|
|||
/// <param name="characterArray">character array</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
/// <param name="characterLength">character length</param>
|
|||
public CharacterBufferRange( |
|||
char[] characterArray, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) |
|||
: this( |
|||
new CharacterBufferReference(characterArray, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from string
|
|||
/// </summary>
|
|||
/// <param name="characterString">character string</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
/// <param name="characterLength">character length</param>
|
|||
public CharacterBufferRange( |
|||
string characterString, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) |
|||
: this( |
|||
new CharacterBufferReference(characterString, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
|
|||
/// </summary>
|
|||
/// <param name="characterBufferReference">character buffer reference</param>
|
|||
/// <param name="characterLength">number of characters</param>
|
|||
public CharacterBufferRange( |
|||
CharacterBufferReference characterBufferReference, |
|||
int characterLength |
|||
) |
|||
{ |
|||
if (characterLength < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); |
|||
} |
|||
|
|||
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? |
|||
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : |
|||
0; |
|||
|
|||
if (characterLength > maxLength) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); |
|||
} |
|||
|
|||
CharacterBufferReference = characterBufferReference; |
|||
Length = characterLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
CharacterBufferRange characterBufferRange, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) : |
|||
this( |
|||
characterBufferRange.CharacterBuffer, |
|||
characterBufferRange.OffsetToFirstChar + offsetToFirstChar, |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from string
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
string charString |
|||
) : |
|||
this( |
|||
charString, |
|||
0, |
|||
charString.Length |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
ReadOnlyMemory<char> charBuffer, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) : |
|||
this( |
|||
new CharacterBufferReference(charBuffer, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
|
|||
/// </summary>
|
|||
internal CharacterBufferRange(TextRun textRun) |
|||
{ |
|||
CharacterBufferReference = textRun.CharacterBufferReference; |
|||
Length = textRun.Length; |
|||
} |
|||
|
|||
public char this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
#if DEBUG
|
|||
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
} |
|||
#endif
|
|||
return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the character buffer
|
|||
/// </summary>
|
|||
public CharacterBufferReference CharacterBufferReference { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of characters in text source character store
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a span from the character buffer range
|
|||
/// </summary>
|
|||
public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); |
|||
|
|||
/// <summary>
|
|||
/// Gets the character memory buffer
|
|||
/// </summary>
|
|||
internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer; |
|||
|
|||
/// <summary>
|
|||
/// Gets the character offset relative to the beginning of buffer to
|
|||
/// the first character of the run
|
|||
/// </summary>
|
|||
internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar; |
|||
|
|||
/// <summary>
|
|||
/// Indicate whether the character buffer range is empty
|
|||
/// </summary>
|
|||
internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; |
|||
|
|||
internal CharacterBufferRange Take(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new CharacterBufferRange(CharacterBufferReference, length); |
|||
} |
|||
|
|||
internal CharacterBufferRange Skip(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
if (length == Length) |
|||
{ |
|||
return new CharacterBufferRange(new CharacterBufferReference(), 0); |
|||
} |
|||
|
|||
var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); |
|||
|
|||
return new CharacterBufferRange(characterBufferReference, Length - length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compute hash code
|
|||
/// </summary>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return CharacterBufferReference.GetHashCode() ^ Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input object
|
|||
/// </summary>
|
|||
/// <param name="obj"> The object to test </param>
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
if (obj is CharacterBufferRange range) |
|||
{ |
|||
return Equals(range); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input CharacterBufferRange
|
|||
/// </summary>
|
|||
/// <param name="value"> The CharacterBufferRange value to test </param>
|
|||
public bool Equals(CharacterBufferRange value) |
|||
{ |
|||
return CharacterBufferReference.Equals(value.CharacterBufferReference) |
|||
&& Length == value.Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferRange for equality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferRange for inequality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) |
|||
{ |
|||
return !(left == right); |
|||
} |
|||
|
|||
int IReadOnlyCollection<char>.Count => Length; |
|||
|
|||
public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -1,115 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
/// <summary>
|
|||
/// Text character buffer reference
|
|||
/// </summary>
|
|||
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference> |
|||
{ |
|||
/// <summary>
|
|||
/// Construct character buffer reference from character array
|
|||
/// </summary>
|
|||
/// <param name="characterArray">character array</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) |
|||
: this(characterArray.AsMemory(), offsetToFirstChar) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct character buffer reference from string
|
|||
/// </summary>
|
|||
/// <param name="characterString">character string</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) |
|||
: this(characterString.AsMemory(), offsetToFirstChar) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct character buffer reference from memory buffer
|
|||
/// </summary>
|
|||
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0) |
|||
{ |
|||
if (offsetToFirstChar < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); |
|||
} |
|||
|
|||
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
|
|||
// even in the case of an empty or null character buffer
|
|||
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); |
|||
if (offsetToFirstChar > maxOffset) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); |
|||
} |
|||
|
|||
CharacterBuffer = characterBuffer; |
|||
OffsetToFirstChar = offsetToFirstChar; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the character memory buffer
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<char> CharacterBuffer { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the character offset relative to the beginning of buffer to
|
|||
/// the first character of the run
|
|||
/// </summary>
|
|||
public int OffsetToFirstChar { get; } |
|||
|
|||
/// <summary>
|
|||
/// Compute hash code
|
|||
/// </summary>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input object
|
|||
/// </summary>
|
|||
/// <param name="obj"> The object to test. </param>
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
if (obj is CharacterBufferReference reference) |
|||
{ |
|||
return Equals(reference); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input CharacterBufferReference
|
|||
/// </summary>
|
|||
/// <param name="value"> The characterBufferReference value to test </param>
|
|||
public bool Equals(CharacterBufferReference value) |
|||
{ |
|||
return CharacterBuffer.Equals(value.CharacterBuffer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferReference for equality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferReference for inequality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) |
|||
{ |
|||
return !(left == right); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,68 @@ |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Platform; |
|||
|
|||
/// <summary>
|
|||
/// System theme variant or mode.
|
|||
/// </summary>
|
|||
public enum PlatformThemeVariant |
|||
{ |
|||
Light, |
|||
Dark |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// System high contrast preference.
|
|||
/// </summary>
|
|||
public enum ColorContrastPreference |
|||
{ |
|||
NoPreference, |
|||
High |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Information about current system color values, including information about dark mode and accent colors.
|
|||
/// </summary>
|
|||
public record PlatformColorValues |
|||
{ |
|||
private static Color DefaultAccent => new(255, 0, 120, 215); |
|||
private Color _accentColor2, _accentColor3; |
|||
|
|||
/// <summary>
|
|||
/// System theme variant or mode.
|
|||
/// </summary>
|
|||
public PlatformThemeVariant ThemeVariant { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// System high contrast preference.
|
|||
/// </summary>
|
|||
public ColorContrastPreference ContrastPreference { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Primary system accent color.
|
|||
/// </summary>
|
|||
public Color AccentColor1 { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Secondary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
|
|||
/// </summary>
|
|||
public Color AccentColor2 |
|||
{ |
|||
get => _accentColor2 != default ? _accentColor2 : AccentColor1; |
|||
init => _accentColor2 = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tertiary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
|
|||
/// </summary>
|
|||
public Color AccentColor3 |
|||
{ |
|||
get => _accentColor3 != default ? _accentColor3 : AccentColor1; |
|||
init => _accentColor3 = value; |
|||
} |
|||
|
|||
public PlatformColorValues() |
|||
{ |
|||
AccentColor1 = DefaultAccent; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
[Unstable] |
|||
public interface ITopLevelWithSystemNavigationManager |
|||
{ |
|||
ISystemNavigationManager SystemNavigationManager { get; } |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface ISystemNavigationManager |
|||
{ |
|||
public event EventHandler<RoutedEventArgs>? BackRequested; |
|||
} |
|||
} |
|||
@ -1,114 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Creates containers for items and maintains a list of created containers.
|
|||
/// </summary>
|
|||
public interface IItemContainerGenerator |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the currently realized containers.
|
|||
/// </summary>
|
|||
IEnumerable<ItemContainerInfo> Containers { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the theme to be applied to the items in the control.
|
|||
/// </summary>
|
|||
ControlTheme? ItemContainerTheme { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the data template used to display the items in the control.
|
|||
/// </summary>
|
|||
IDataTemplate? ItemTemplate { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the binding to use to bind to the member of an item used for displaying
|
|||
/// </summary>
|
|||
IBinding? DisplayMemberBinding { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the ContainerType, or null if its an untyped ContainerGenerator.
|
|||
/// </summary>
|
|||
Type? ContainerType { get; } |
|||
|
|||
/// <summary>
|
|||
/// Signaled whenever new containers are materialized.
|
|||
/// </summary>
|
|||
event EventHandler<ItemContainerEventArgs>? Materialized; |
|||
|
|||
/// <summary>
|
|||
/// Event raised whenever containers are dematerialized.
|
|||
/// </summary>
|
|||
event EventHandler<ItemContainerEventArgs>? Dematerialized; |
|||
|
|||
/// <summary>
|
|||
/// Event raised whenever containers are recycled.
|
|||
/// </summary>
|
|||
event EventHandler<ItemContainerEventArgs>? Recycled; |
|||
|
|||
/// <summary>
|
|||
/// Creates a container control for an item.
|
|||
/// </summary>
|
|||
/// <param name="index">
|
|||
/// The index of the item of data in the control's items.
|
|||
/// </param>
|
|||
/// <param name="item">The item.</param>
|
|||
/// <returns>The created controls.</returns>
|
|||
ItemContainerInfo Materialize(int index, object item); |
|||
|
|||
/// <summary>
|
|||
/// Removes a set of created containers.
|
|||
/// </summary>
|
|||
/// <param name="startingIndex">
|
|||
/// The index of the first item in the control's items.
|
|||
/// </param>
|
|||
/// <param name="count">The the number of items to remove.</param>
|
|||
/// <returns>The removed containers.</returns>
|
|||
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count); |
|||
|
|||
/// <summary>
|
|||
/// Inserts space for newly inserted containers in the index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index at which space should be inserted.</param>
|
|||
/// <param name="count">The number of blank spaces to create.</param>
|
|||
void InsertSpace(int index, int count); |
|||
|
|||
/// <summary>
|
|||
/// Removes a set of created containers and updates the index of later containers to fill
|
|||
/// the gap.
|
|||
/// </summary>
|
|||
/// <param name="startingIndex">
|
|||
/// The index of the first item in the control's items.
|
|||
/// </param>
|
|||
/// <param name="count">The the number of items to remove.</param>
|
|||
/// <returns>The removed containers.</returns>
|
|||
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count); |
|||
|
|||
bool TryRecycle(int oldIndex, int newIndex, object item); |
|||
|
|||
/// <summary>
|
|||
/// Clears all created containers and returns the removed controls.
|
|||
/// </summary>
|
|||
/// <returns>The removed controls.</returns>
|
|||
IEnumerable<ItemContainerInfo> Clear(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the container control representing the item with the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index.</param>
|
|||
/// <returns>The container, or null if no container created.</returns>
|
|||
Control? ContainerFromIndex(int index); |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the specified container control.
|
|||
/// </summary>
|
|||
/// <param name="container">The container.</param>
|
|||
/// <returns>The index of the container, or -1 if not found.</returns>
|
|||
int IndexFromContainer(Control? container); |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Creates containers for tree items and maintains a list of created containers.
|
|||
/// </summary>
|
|||
public interface ITreeItemContainerGenerator : IItemContainerGenerator |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the container index for the tree.
|
|||
/// </summary>
|
|||
TreeContainerIndex? Index { get; } |
|||
|
|||
/// <summary>
|
|||
/// Updates the index based on the parent <see cref="TreeView"/>.
|
|||
/// </summary>
|
|||
void UpdateIndex(); |
|||
} |
|||
} |
|||
@ -1,49 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
|
|||
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
|
|||
/// </summary>
|
|||
public class ItemContainerEventArgs : EventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
|
|||
/// </summary>
|
|||
/// <param name="container">The container.</param>
|
|||
public ItemContainerEventArgs(ItemContainerInfo container) |
|||
{ |
|||
StartingIndex = container.Index; |
|||
Containers = new[] { container }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
|
|||
/// </summary>
|
|||
/// <param name="startingIndex">The index of the first container in the source items.</param>
|
|||
/// <param name="containers">The containers.</param>
|
|||
/// <remarks>
|
|||
/// TODO: Do we really need to pass in StartingIndex here? The ItemContainerInfo objects
|
|||
/// have an index, and what happens if the contains passed in aren't sequential?
|
|||
/// </remarks>
|
|||
public ItemContainerEventArgs( |
|||
int startingIndex, |
|||
IList<ItemContainerInfo> containers) |
|||
{ |
|||
StartingIndex = startingIndex; |
|||
Containers = containers; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the containers.
|
|||
/// </summary>
|
|||
public IList<ItemContainerInfo> Containers { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the first container in the source items.
|
|||
/// </summary>
|
|||
public int StartingIndex { get; } |
|||
} |
|||
} |
|||
@ -1,262 +1,113 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Creates containers for items and maintains a list of created containers.
|
|||
/// Generates containers for an <see cref="ItemsControl"/>.
|
|||
/// </summary>
|
|||
public class ItemContainerGenerator : IItemContainerGenerator |
|||
/// <remarks>
|
|||
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
|
|||
/// method order should be followed:
|
|||
///
|
|||
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
|
|||
/// derived from the <see cref="Control"/> class. If this method returns true then the
|
|||
/// item itself should be used as the container.
|
|||
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
|
|||
/// <see cref="CreateContainer"/> should be called to create a new container.
|
|||
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
|
|||
/// container.
|
|||
/// - The container should then be added to the panel using
|
|||
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
|
|||
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
|
|||
/// - When the item is ready to be recycled, <see cref="ClearItemContainer(Control)"/> should
|
|||
/// be called if <see cref="IsItemItsOwnContainer(Control)"/> returned false.
|
|||
///
|
|||
/// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only
|
|||
/// concerns itself with generating and clearing item containers; it does not maintain a
|
|||
/// record of the currently realized containers, that responsibility is delegated to the
|
|||
/// items panel.
|
|||
/// </remarks>
|
|||
public class ItemContainerGenerator |
|||
{ |
|||
private SortedDictionary<int, ItemContainerInfo> _containers = new SortedDictionary<int, ItemContainerInfo>(); |
|||
private readonly ItemsControl _owner; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner control.</param>
|
|||
public ItemContainerGenerator(Control owner) |
|||
{ |
|||
Owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<ItemContainerInfo> Containers => _containers.Values; |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler<ItemContainerEventArgs>? Materialized; |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler<ItemContainerEventArgs>? Dematerialized; |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler<ItemContainerEventArgs>? Recycled; |
|||
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the theme to be applied to the items in the control.
|
|||
/// Creates a new container control.
|
|||
/// </summary>
|
|||
public ControlTheme? ItemContainerTheme { get; set; } |
|||
/// <returns>The newly created container control.</returns>
|
|||
/// <remarks>
|
|||
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
|
|||
/// called to determine whether the item itself should be used as a container. After
|
|||
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
|
|||
/// be called to prepare the container to display the specified item.
|
|||
/// </remarks>
|
|||
public Control CreateContainer() => _owner.CreateContainerForItemOverride(); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the data template used to display the items in the control.
|
|||
/// Determines whether the specified item is (or is eligible to be) its own container.
|
|||
/// </summary>
|
|||
public IDataTemplate? ItemTemplate { get; set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IBinding? DisplayMemberBinding { get; set; } |
|||
/// <param name="container">The item.</param>
|
|||
/// <returns>true if the item is its own container, otherwise false.</returns>
|
|||
/// <remarks>
|
|||
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
|
|||
/// control items may be; the caller is responsible for checking if each item is a control
|
|||
/// and calling this method before creating a new container.
|
|||
/// </remarks>
|
|||
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container); |
|||
|
|||
/// <summary>
|
|||
/// Gets the owner control.
|
|||
/// Prepares the specified element as the container for the corresponding item.
|
|||
/// </summary>
|
|||
public Control Owner { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual Type? ContainerType => null; |
|||
|
|||
/// <inheritdoc/>
|
|||
public ItemContainerInfo Materialize(int index, object item) |
|||
{ |
|||
var container = new ItemContainerInfo(CreateContainer(item)!, item, index); |
|||
|
|||
_containers.Add(container.Index, container); |
|||
Materialized?.Invoke(this, new ItemContainerEventArgs(container)); |
|||
|
|||
return container; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count) |
|||
{ |
|||
var result = new List<ItemContainerInfo>(); |
|||
|
|||
for (int i = startingIndex; i < startingIndex + count; ++i) |
|||
{ |
|||
result.Add(_containers[i]); |
|||
_containers.Remove(i); |
|||
} |
|||
|
|||
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void InsertSpace(int index, int count) |
|||
{ |
|||
if (count > 0) |
|||
{ |
|||
var toMove = _containers.Where(x => x.Key >= index) |
|||
.OrderByDescending(x => x.Key) |
|||
.ToArray(); |
|||
|
|||
foreach (var i in toMove) |
|||
{ |
|||
_containers.Remove(i.Key); |
|||
i.Value.Index += count; |
|||
_containers.Add(i.Value.Index, i.Value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count) |
|||
{ |
|||
var result = new List<ItemContainerInfo>(); |
|||
|
|||
if (count > 0) |
|||
{ |
|||
for (var i = startingIndex; i < startingIndex + count; ++i) |
|||
{ |
|||
if (_containers.TryGetValue(i, out var found)) |
|||
{ |
|||
result.Add(found); |
|||
} |
|||
|
|||
_containers.Remove(i); |
|||
} |
|||
|
|||
var toMove = _containers.Where(x => x.Key >= startingIndex) |
|||
.OrderBy(x => x.Key).ToArray(); |
|||
|
|||
foreach (var i in toMove) |
|||
{ |
|||
_containers.Remove(i.Key); |
|||
i.Value.Index -= count; |
|||
_containers.Add(i.Value.Index, i.Value); |
|||
} |
|||
|
|||
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); |
|||
|
|||
if (toMove.Length > 0) |
|||
{ |
|||
var containers = toMove.Select(x => x.Value).ToArray(); |
|||
Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers)); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual IEnumerable<ItemContainerInfo> Clear() |
|||
{ |
|||
var result = Containers.ToArray(); |
|||
_containers.Clear(); |
|||
|
|||
if (result.Length > 0) |
|||
{ |
|||
Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Control? ContainerFromIndex(int index) |
|||
{ |
|||
ItemContainerInfo? result; |
|||
_containers.TryGetValue(index, out result); |
|||
return result?.ContainerControl; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int IndexFromContainer(Control? container) |
|||
{ |
|||
foreach (var i in _containers) |
|||
{ |
|||
if (i.Value.ContainerControl == container) |
|||
{ |
|||
return i.Key; |
|||
} |
|||
} |
|||
|
|||
return -1; |
|||
} |
|||
/// <param name="container">The element that's used to display the specified item.</param>
|
|||
/// <param name="item">The item to display.</param>
|
|||
/// <param name="index">The index of the item to display.</param>
|
|||
/// <remarks>
|
|||
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
|
|||
/// only needs to be called a single time, otherwise this method should be called after the
|
|||
/// container is created, and each subsequent time the container is recycled to display a
|
|||
/// new item.
|
|||
/// </remarks>
|
|||
public void PrepareItemContainer(Control container, object? item, int index) => |
|||
_owner.PrepareItemContainer(container, item, index); |
|||
|
|||
/// <summary>
|
|||
/// Creates the container for an item.
|
|||
/// Notifies the <see cref="ItemsControl"/> that a container has been fully prepared to
|
|||
/// display an item.
|
|||
/// </summary>
|
|||
/// <param name="item">The item.</param>
|
|||
/// <returns>The created container control.</returns>
|
|||
protected virtual Control? CreateContainer(object item) |
|||
{ |
|||
var result = item as Control; |
|||
|
|||
if (result == null) |
|||
{ |
|||
result = new ContentPresenter(); |
|||
if (DisplayMemberBinding is not null) |
|||
{ |
|||
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style); |
|||
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style); |
|||
} |
|||
else |
|||
{ |
|||
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); |
|||
} |
|||
|
|||
if (ItemTemplate != null) |
|||
{ |
|||
result.SetValue( |
|||
ContentPresenter.ContentTemplateProperty, |
|||
ItemTemplate, |
|||
BindingPriority.Style); |
|||
} |
|||
} |
|||
|
|||
if (ItemContainerTheme != null) |
|||
{ |
|||
result.SetValue( |
|||
StyledElement.ThemeProperty, |
|||
ItemContainerTheme, |
|||
BindingPriority.Template); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
/// <param name="container">The container control.</param>
|
|||
/// <param name="item">The item being displayed.</param>
|
|||
/// <param name="index">The index of the item being displayed.</param>
|
|||
/// <remarks>
|
|||
/// This method should be called when a container has been fully prepared and added
|
|||
/// to the logical and visual trees, but may be called before a layout pass has completed.
|
|||
/// It should be called regardless of the result of
|
|||
/// <see cref="IsItemItsOwnContainer(Control)"/>.
|
|||
/// </remarks>
|
|||
public void ItemContainerPrepared(Control container, object? item, int index) => |
|||
_owner.ItemContainerPrepared(container, item, index); |
|||
|
|||
/// <summary>
|
|||
/// Moves a container.
|
|||
/// Called when the index for a container changes due to an insertion or removal in the
|
|||
/// items collection.
|
|||
/// </summary>
|
|||
/// <param name="container">The container whose index changed.</param>
|
|||
/// <param name="oldIndex">The old index.</param>
|
|||
/// <param name="newIndex">The new index.</param>
|
|||
/// <param name="item">The new item.</param>
|
|||
/// <returns>The container info.</returns>
|
|||
protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) |
|||
{ |
|||
var container = _containers[oldIndex]; |
|||
container.Index = newIndex; |
|||
container.Item = item; |
|||
_containers.Remove(oldIndex); |
|||
_containers.Add(newIndex, container); |
|||
return container; |
|||
} |
|||
public void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) => |
|||
_owner.ItemContainerIndexChanged(container, oldIndex, newIndex); |
|||
|
|||
/// <summary>
|
|||
/// Gets all containers with an index that fall within a range.
|
|||
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
|
|||
/// </summary>
|
|||
/// <param name="index">The first index.</param>
|
|||
/// <param name="count">The number of elements in the range.</param>
|
|||
/// <returns>The containers.</returns>
|
|||
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count) |
|||
{ |
|||
return _containers.Where(x => x.Key >= index && x.Key < index + count).Select(x => x.Value); |
|||
} |
|||
/// <param name="container">The container control.</param>
|
|||
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container); |
|||
|
|||
/// <summary>
|
|||
/// Raises the <see cref="Recycled"/> event.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
protected void RaiseRecycled(ItemContainerEventArgs e) |
|||
{ |
|||
Recycled?.Invoke(this, e); |
|||
} |
|||
[Obsolete("Use ItemsControl.ContainerFromIndex")] |
|||
public Control? ContainerFromIndex(int index) => _owner.ContainerFromIndex(index); |
|||
|
|||
[Obsolete("Use ItemsControl.IndexFromContainer")] |
|||
public int IndexFromContainer(Control container) => _owner.IndexFromContainer(container); |
|||
} |
|||
} |
|||
|
|||
@ -1,102 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Creates containers for items and maintains a list of created containers.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the container.</typeparam>
|
|||
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : Control, new() |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner control.</param>
|
|||
/// <param name="contentProperty">The container's Content property.</param>
|
|||
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
|
|||
public ItemContainerGenerator( |
|||
Control owner, |
|||
AvaloniaProperty contentProperty, |
|||
AvaloniaProperty? contentTemplateProperty) |
|||
: base(owner) |
|||
{ |
|||
ContentProperty = contentProperty ?? throw new ArgumentNullException(nameof(contentProperty)); |
|||
ContentTemplateProperty = contentTemplateProperty; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override Type ContainerType => typeof(T); |
|||
|
|||
/// <summary>
|
|||
/// Gets the container's Content property.
|
|||
/// </summary>
|
|||
protected AvaloniaProperty ContentProperty { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the container's ContentTemplate property.
|
|||
/// </summary>
|
|||
protected AvaloniaProperty? ContentTemplateProperty { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Control? CreateContainer(object item) |
|||
{ |
|||
var container = item as T; |
|||
|
|||
if (container is null) |
|||
{ |
|||
container = new T(); |
|||
|
|||
if (ContentTemplateProperty != null) |
|||
{ |
|||
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); |
|||
} |
|||
|
|||
if (DisplayMemberBinding is not null) |
|||
{ |
|||
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style); |
|||
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style); |
|||
} |
|||
else |
|||
{ |
|||
container.SetValue(ContentProperty, item, BindingPriority.Style); |
|||
} |
|||
|
|||
if (!(item is Control)) |
|||
{ |
|||
container.DataContext = item; |
|||
} |
|||
} |
|||
|
|||
if (ItemContainerTheme != null) |
|||
{ |
|||
container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style); |
|||
} |
|||
|
|||
return container; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool TryRecycle(int oldIndex, int newIndex, object item) |
|||
{ |
|||
var container = ContainerFromIndex(oldIndex); |
|||
|
|||
if (container == null) |
|||
{ |
|||
throw new IndexOutOfRangeException("Could not recycle container: not materialized."); |
|||
} |
|||
|
|||
container.SetValue(ContentProperty, item); |
|||
|
|||
if (!(item is Control)) |
|||
{ |
|||
container.DataContext = item; |
|||
} |
|||
|
|||
var info = MoveContainer(oldIndex, newIndex, item); |
|||
RaiseRecycled(new ItemContainerEventArgs(info)); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Holds information about an item container generated by an
|
|||
/// <see cref="IItemContainerGenerator"/>.
|
|||
/// </summary>
|
|||
public class ItemContainerInfo |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
|
|||
/// </summary>
|
|||
/// <param name="container">The container control.</param>
|
|||
/// <param name="item">The item that the container represents.</param>
|
|||
/// <param name="index">
|
|||
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
|
|||
/// </param>
|
|||
public ItemContainerInfo(Control container, object item, int index) |
|||
{ |
|||
ContainerControl = container; |
|||
Item = item; |
|||
Index = index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the container control.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This will be null if <see cref="Item"/> is null.
|
|||
/// </remarks>
|
|||
public Control ContainerControl { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the item that the container represents.
|
|||
/// </summary>
|
|||
public object Item { get; internal set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
|
|||
/// </summary>
|
|||
public int Index { get; set; } |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
public class MenuItemContainerGenerator : ItemContainerGenerator<MenuItem> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner control.</param>
|
|||
public MenuItemContainerGenerator(Control owner) |
|||
: base(owner, MenuItem.HeaderProperty, null) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Control? CreateContainer(object item) |
|||
{ |
|||
var separator = item as Separator; |
|||
return separator != null ? separator : base.CreateContainer(item); |
|||
} |
|||
} |
|||
} |
|||
@ -1,105 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem> |
|||
{ |
|||
public TabItemContainerGenerator(TabControl owner) |
|||
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty) |
|||
{ |
|||
Owner = owner; |
|||
} |
|||
|
|||
public new TabControl Owner { get; } |
|||
|
|||
protected override Control CreateContainer(object item) |
|||
{ |
|||
var tabItem = (TabItem)base.CreateContainer(item)!; |
|||
|
|||
tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>( |
|||
tabItem, |
|||
TabControl.TabStripPlacementProperty)); |
|||
|
|||
if (tabItem.HeaderTemplate == null) |
|||
{ |
|||
tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate?>( |
|||
tabItem, |
|||
TabControl.ItemTemplateProperty)); |
|||
} |
|||
|
|||
if (Owner.HeaderDisplayMemberBinding is not null) |
|||
{ |
|||
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding, |
|||
BindingPriority.Style); |
|||
} |
|||
|
|||
if (tabItem.Header == null) |
|||
{ |
|||
if (item is IHeadered headered) |
|||
{ |
|||
tabItem.Header = headered.Header; |
|||
} |
|||
else |
|||
{ |
|||
if (!(tabItem.DataContext is Control)) |
|||
{ |
|||
tabItem.Header = tabItem.DataContext; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (!(tabItem.Content is Control)) |
|||
{ |
|||
tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate?>( |
|||
tabItem, |
|||
TabControl.ContentTemplateProperty)); |
|||
} |
|||
|
|||
return tabItem; |
|||
} |
|||
|
|||
private class OwnerBinding<T> : SingleSubscriberObservableBase<T> |
|||
{ |
|||
private readonly TabItem _item; |
|||
private readonly StyledProperty<T> _ownerProperty; |
|||
private IDisposable? _ownerSubscription; |
|||
private IDisposable? _propertySubscription; |
|||
|
|||
public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty) |
|||
{ |
|||
_item = item; |
|||
_ownerProperty = ownerProperty; |
|||
} |
|||
|
|||
protected override void Subscribed() |
|||
{ |
|||
_ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged); |
|||
} |
|||
|
|||
protected override void Unsubscribed() |
|||
{ |
|||
_ownerSubscription?.Dispose(); |
|||
_ownerSubscription = null; |
|||
} |
|||
|
|||
private void OwnerChanged(ILogical? c) |
|||
{ |
|||
_propertySubscription?.Dispose(); |
|||
_propertySubscription = null; |
|||
|
|||
if (c is TabControl tabControl) |
|||
{ |
|||
_propertySubscription = tabControl.GetObservable(_ownerProperty) |
|||
.Subscribe(x => PublishNext(x)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,166 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Maintains an index of all item containers currently materialized by a <see cref="TreeView"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Each <see cref="TreeViewItem"/> has its own <see cref="TreeItemContainerGenerator{T}"/>
|
|||
/// that maintains the list of its direct children, but they also share an instance of this
|
|||
/// class in their <see cref="TreeItemContainerGenerator{T}.Index"/> property which tracks
|
|||
/// the containers materialized for the entire tree.
|
|||
/// </remarks>
|
|||
public class TreeContainerIndex |
|||
{ |
|||
private readonly Dictionary<object, HashSet<Control>> _itemToContainerSet = new Dictionary<object, HashSet<Control>>(); |
|||
private readonly Dictionary<object, Control> _itemToContainer = new Dictionary<object, Control>(); |
|||
private readonly Dictionary<Control, object> _containerToItem = new Dictionary<Control, object>(); |
|||
|
|||
/// <summary>
|
|||
/// Signaled whenever new containers are materialized.
|
|||
/// </summary>
|
|||
public event EventHandler<ItemContainerEventArgs>? Materialized; |
|||
|
|||
/// <summary>
|
|||
/// Event raised whenever containers are dematerialized.
|
|||
/// </summary>
|
|||
public event EventHandler<ItemContainerEventArgs>? Dematerialized; |
|||
|
|||
/// <summary>
|
|||
/// Gets the currently materialized containers.
|
|||
/// </summary>
|
|||
public IEnumerable<Control> Containers => _containerToItem.Keys; |
|||
|
|||
/// <summary>
|
|||
/// Gets the items of currently materialized containers.
|
|||
/// </summary>
|
|||
public IEnumerable<object> Items => _containerToItem.Values; |
|||
|
|||
/// <summary>
|
|||
/// Adds an entry to the index.
|
|||
/// </summary>
|
|||
/// <param name="item">The item.</param>
|
|||
/// <param name="container">The item container.</param>
|
|||
public void Add(object item, Control container) |
|||
{ |
|||
_itemToContainer[item] = container; |
|||
if (_itemToContainerSet.TryGetValue(item, out var set)) |
|||
{ |
|||
set.Add(container); |
|||
} |
|||
else |
|||
{ |
|||
_itemToContainerSet.Add(item, new HashSet<Control> { container }); |
|||
} |
|||
|
|||
_containerToItem.Add(container, item); |
|||
|
|||
Materialized?.Invoke( |
|||
this, |
|||
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a container from private collections.
|
|||
/// </summary>
|
|||
/// <param name="container">The item container.</param>
|
|||
/// <param name="item">The DataContext object</param>
|
|||
private void RemoveContainer(Control container, object item) |
|||
{ |
|||
if (_itemToContainerSet.TryGetValue(item, out var set)) |
|||
{ |
|||
set.Remove(container); |
|||
if (set.Count == 0) |
|||
{ |
|||
_itemToContainerSet.Remove(item); |
|||
_itemToContainer.Remove(item); |
|||
} |
|||
else |
|||
{ |
|||
_itemToContainer[item] = set.First(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a container from the index.
|
|||
/// </summary>
|
|||
/// <param name="container">The item container.</param>
|
|||
public void Remove(Control container) |
|||
{ |
|||
var item = _containerToItem[container]; |
|||
_containerToItem.Remove(container); |
|||
RemoveContainer(container, item); |
|||
|
|||
Dematerialized?.Invoke( |
|||
this, |
|||
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a set of containers from the index.
|
|||
/// </summary>
|
|||
/// <param name="startingIndex">The index of the first item.</param>
|
|||
/// <param name="containers">The item containers.</param>
|
|||
public void Remove(int startingIndex, IEnumerable<ItemContainerInfo> containers) |
|||
{ |
|||
foreach (var container in containers) |
|||
{ |
|||
var item = _containerToItem[container.ContainerControl]; |
|||
_containerToItem.Remove(container.ContainerControl); |
|||
RemoveContainer(container.ContainerControl, item); |
|||
} |
|||
|
|||
Dematerialized?.Invoke( |
|||
this, |
|||
new ItemContainerEventArgs(startingIndex, containers.ToList())); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the container for an item.
|
|||
/// </summary>
|
|||
/// <param name="item">The item.</param>
|
|||
/// <returns>The container, or null of not found.</returns>
|
|||
public Control? ContainerFromItem(object item) |
|||
{ |
|||
if (item != null) |
|||
{ |
|||
_itemToContainer.TryGetValue(item, out var result); |
|||
if (result == null) |
|||
{ |
|||
_itemToContainerSet.TryGetValue(item, out var set); |
|||
if (set?.Count > 0) |
|||
{ |
|||
return set.FirstOrDefault(); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the item for a container.
|
|||
/// </summary>
|
|||
/// <param name="container">The container.</param>
|
|||
/// <returns>The item, or null of not found.</returns>
|
|||
public object? ItemFromContainer(Control? container) |
|||
{ |
|||
if (container != null) |
|||
{ |
|||
_containerToItem.TryGetValue(container, out var result); |
|||
if (result != null) |
|||
{ |
|||
_itemToContainer[result] = container; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,164 +1,32 @@ |
|||
using System; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
/// <summary>
|
|||
/// Creates containers for tree items and maintains a list of created containers.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the container.</typeparam>
|
|||
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator |
|||
where T : Control, new() |
|||
public class TreeItemContainerGenerator : ItemContainerGenerator |
|||
{ |
|||
private TreeView? _treeView; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner control.</param>
|
|||
/// <param name="contentProperty">The container's Content property.</param>
|
|||
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
|
|||
/// <param name="itemsProperty">The container's Items property.</param>
|
|||
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
|
|||
public TreeItemContainerGenerator( |
|||
Control owner, |
|||
AvaloniaProperty contentProperty, |
|||
AvaloniaProperty contentTemplateProperty, |
|||
AvaloniaProperty itemsProperty, |
|||
AvaloniaProperty isExpandedProperty) |
|||
: base(owner, contentProperty, contentTemplateProperty) |
|||
{ |
|||
ItemsProperty = itemsProperty ?? throw new ArgumentNullException(nameof(itemsProperty)); |
|||
IsExpandedProperty = isExpandedProperty ?? throw new ArgumentNullException(nameof(isExpandedProperty)); |
|||
UpdateIndex(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the container index for the tree.
|
|||
/// </summary>
|
|||
public TreeContainerIndex? Index { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the item container's Items property.
|
|||
/// </summary>
|
|||
protected AvaloniaProperty ItemsProperty { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the item container's IsExpanded property.
|
|||
/// </summary>
|
|||
protected AvaloniaProperty IsExpandedProperty { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Control? CreateContainer(object? item) |
|||
{ |
|||
var container = item as T; |
|||
|
|||
if (item == null) |
|||
{ |
|||
return null; |
|||
} |
|||
else if (container != null) |
|||
{ |
|||
Index?.Add(item, container); |
|||
return container; |
|||
} |
|||
else |
|||
{ |
|||
var template = GetTreeDataTemplate(item, ItemTemplate); |
|||
var result = new T(); |
|||
|
|||
if (ItemContainerTheme != null) |
|||
{ |
|||
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style); |
|||
} |
|||
|
|||
if (DisplayMemberBinding is not null) |
|||
{ |
|||
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style); |
|||
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style); |
|||
} |
|||
else |
|||
{ |
|||
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); |
|||
} |
|||
|
|||
var itemsSelector = template.ItemsSelector(item); |
|||
|
|||
if (itemsSelector != null) |
|||
{ |
|||
BindingOperations.Apply(result, ItemsProperty, itemsSelector, null); |
|||
} |
|||
|
|||
if (!(item is Control)) |
|||
{ |
|||
result.DataContext = item; |
|||
} |
|||
|
|||
Index?.Add(item, result); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
public override IEnumerable<ItemContainerInfo> Clear() |
|||
internal TreeItemContainerGenerator(TreeView owner) |
|||
: base(owner) |
|||
{ |
|||
var items = base.Clear(); |
|||
Index?.Remove(0, items); |
|||
return items; |
|||
Index = new TreeContainerIndex(owner); |
|||
} |
|||
|
|||
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count) |
|||
{ |
|||
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); |
|||
return base.Dematerialize(startingIndex, count); |
|||
} |
|||
public TreeContainerIndex Index { get; } |
|||
} |
|||
|
|||
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count) |
|||
{ |
|||
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); |
|||
return base.RemoveRange(startingIndex, count); |
|||
} |
|||
public class TreeContainerIndex |
|||
{ |
|||
private readonly TreeView _owner; |
|||
|
|||
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; |
|||
internal TreeContainerIndex(TreeView owner) => _owner = owner; |
|||
|
|||
public void UpdateIndex() |
|||
{ |
|||
if (Owner is TreeView treeViewOwner && Index == null) |
|||
{ |
|||
Index = new TreeContainerIndex(); |
|||
_treeView = treeViewOwner; |
|||
} |
|||
else |
|||
{ |
|||
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault(); |
|||
|
|||
if (treeView != _treeView) |
|||
{ |
|||
Clear(); |
|||
Index = treeView?.ItemContainerGenerator?.Index; |
|||
_treeView = treeView; |
|||
} |
|||
} |
|||
} |
|||
[Obsolete("Use TreeView.GetRealizedTreeContainers")] |
|||
public IEnumerable<Control> Containers => _owner.GetRealizedTreeContainers(); |
|||
|
|||
class WrapperTreeDataTemplate : ITreeDataTemplate |
|||
{ |
|||
private readonly IDataTemplate _inner; |
|||
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; |
|||
public Control? Build(object? param) => _inner.Build(param); |
|||
public bool Match(object? data) => _inner.Match(data); |
|||
public InstancedBinding? ItemsSelector(object item) => null; |
|||
} |
|||
[Obsolete("Use TreeView.TreeContainerFromItem")] |
|||
public Control? ContainerFromItem(object item) => _owner.TreeContainerFromItem(item); |
|||
|
|||
private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary) |
|||
{ |
|||
var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; |
|||
var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); |
|||
return treeTemplate; |
|||
} |
|||
[Obsolete("Use TreeView.TreeItemFromContainer")] |
|||
public object? ItemFromContainer(Control container) => _owner.TreeItemFromContainer(container); |
|||
} |
|||
} |
|||
|
|||
@ -1,19 +0,0 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Interface implemented by controls that act as controllers for an
|
|||
/// <see cref="IVirtualizingPanel"/>.
|
|||
/// </summary>
|
|||
public interface IVirtualizingController |
|||
{ |
|||
/// <summary>
|
|||
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The controller should respond to this method being called by either adding
|
|||
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
|
|||
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
|
|||
/// </remarks>
|
|||
void UpdateControls(); |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// A panel that can be used to virtualize items.
|
|||
/// </summary>
|
|||
public interface IVirtualizingPanel |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the children of the panel.
|
|||
/// </summary>
|
|||
Controls Children { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the controller for the virtualizing panel.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A virtualizing controller is responsible for maintaining the controls in the virtualizing
|
|||
/// panel. This property will be set by the controller when virtualization is initialized.
|
|||
/// Note that this property may remain null if the panel is added to a control that does
|
|||
/// not act as a virtualizing controller.
|
|||
/// </remarks>
|
|||
IVirtualizingController? Controller { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the panel is full.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should return false until enough children are added to fill the space
|
|||
/// passed into the last measure or arrange in the direction of scroll. It should be
|
|||
/// updated immediately after a child is added or removed.
|
|||
/// </remarks>
|
|||
bool IsFull { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items that can be removed while keeping the panel full.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should return the number of children that are completely out of the
|
|||
/// panel's current bounds in the direction of scroll. It should be updated after an
|
|||
/// arrange.
|
|||
/// </remarks>
|
|||
int OverflowCount { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the direction of scroll.
|
|||
/// </summary>
|
|||
Orientation ScrollDirection { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the average size of the materialized items in the direction of scroll.
|
|||
/// </summary>
|
|||
double AverageItemSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
|
|||
/// direction of scroll.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
|
|||
/// overflows the panel bounds.
|
|||
/// </remarks>
|
|||
double PixelOverflow { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current pixel offset of the items in the direction of scroll.
|
|||
/// </summary>
|
|||
double PixelOffset { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current scroll offset in the cross axis.
|
|||
/// </summary>
|
|||
double CrossAxisOffset { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Invalidates the measure of the control and forces a call to
|
|||
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The implementation for this method should call
|
|||
/// <see cref="Layoutable.InvalidateMeasure"/> and also ensure that the next call to
|
|||
/// <see cref="Layoutable.Measure(Size)"/> calls
|
|||
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure even if
|
|||
/// the available size hasn't changed.
|
|||
/// </remarks>
|
|||
void ForceInvalidateMeasure(); |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Describes the item virtualization method to use for a list.
|
|||
/// </summary>
|
|||
public enum ItemVirtualizationMode |
|||
{ |
|||
/// <summary>
|
|||
/// Do not virtualize items.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Virtualize items without smooth scrolling.
|
|||
/// </summary>
|
|||
Simple, |
|||
} |
|||
} |
|||
@ -1,276 +0,0 @@ |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Reactive; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Displays pages inside an <see cref="ItemsControl"/>.
|
|||
/// </summary>
|
|||
public class CarouselPresenter : ItemsPresenterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="IsVirtualized"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsVirtualizedProperty = |
|||
Carousel.IsVirtualizedProperty.AddOwner<CarouselPresenter>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="SelectedIndex"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<CarouselPresenter, int> SelectedIndexProperty = |
|||
SelectingItemsControl.SelectedIndexProperty.AddOwner<CarouselPresenter>( |
|||
o => o.SelectedIndex, |
|||
(o, v) => o.SelectedIndex = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="PageTransition"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IPageTransition?> PageTransitionProperty = |
|||
Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>(); |
|||
|
|||
private int _selectedIndex = -1; |
|||
private Task? _currentTransition; |
|||
private int _queuedTransitionIndex = -1; |
|||
|
|||
/// <summary>
|
|||
/// Initializes static members of the <see cref="CarouselPresenter"/> class.
|
|||
/// </summary>
|
|||
static CarouselPresenter() |
|||
{ |
|||
IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e)); |
|||
SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// When the carousel is virtualized, only the active page is held in memory.
|
|||
/// </remarks>
|
|||
public bool IsVirtualized |
|||
{ |
|||
get { return GetValue(IsVirtualizedProperty); } |
|||
set { SetValue(IsVirtualizedProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the index of the selected page.
|
|||
/// </summary>
|
|||
public int SelectedIndex |
|||
{ |
|||
get |
|||
{ |
|||
return _selectedIndex; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
var old = SelectedIndex; |
|||
var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1; |
|||
|
|||
if (old != effective) |
|||
{ |
|||
_selectedIndex = effective; |
|||
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a transition to use when switching pages.
|
|||
/// </summary>
|
|||
public IPageTransition? PageTransition |
|||
{ |
|||
get { return GetValue(PageTransitionProperty); } |
|||
set { SetValue(PageTransitionProperty, value); } |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (!IsVirtualized) |
|||
{ |
|||
base.ItemsChanged(e); |
|||
|
|||
if (Items == null || SelectedIndex >= Items.Count()) |
|||
{ |
|||
SelectedIndex = Items.Count() - 1; |
|||
} |
|||
|
|||
foreach (var c in ItemContainerGenerator.Containers) |
|||
{ |
|||
c.ContainerControl.IsVisible = c.Index == SelectedIndex; |
|||
} |
|||
} |
|||
else if (SelectedIndex != -1 && Panel != null) |
|||
{ |
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
if (e.NewStartingIndex > SelectedIndex) |
|||
{ |
|||
return; |
|||
} |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
if (e.OldStartingIndex > SelectedIndex) |
|||
{ |
|||
return; |
|||
} |
|||
break; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
if (e.OldStartingIndex > SelectedIndex || |
|||
e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex) |
|||
{ |
|||
return; |
|||
} |
|||
break; |
|||
case NotifyCollectionChangedAction.Move: |
|||
if (e.OldStartingIndex > SelectedIndex && |
|||
e.NewStartingIndex > SelectedIndex) |
|||
{ |
|||
return; |
|||
} |
|||
break; |
|||
} |
|||
|
|||
if (Items == null || SelectedIndex >= Items.Count()) |
|||
{ |
|||
SelectedIndex = Items.Count() - 1; |
|||
} |
|||
|
|||
Panel.Children.Clear(); |
|||
ItemContainerGenerator.Clear(); |
|||
|
|||
if (SelectedIndex != -1) |
|||
{ |
|||
GetOrCreateContainer(SelectedIndex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void PanelCreated(Panel panel) |
|||
{ |
|||
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
|
|||
/// </summary>
|
|||
/// <param name="fromIndex">The index of the old page.</param>
|
|||
/// <param name="toIndex">The index of the new page.</param>
|
|||
/// <returns>A task tracking the animation.</returns>
|
|||
private async Task MoveToPage(int fromIndex, int toIndex) |
|||
{ |
|||
if (fromIndex != toIndex) |
|||
{ |
|||
var generator = ItemContainerGenerator; |
|||
Control? from = null; |
|||
Control? to = null; |
|||
|
|||
if (fromIndex != -1) |
|||
{ |
|||
from = generator.ContainerFromIndex(fromIndex); |
|||
} |
|||
|
|||
if (toIndex != -1) |
|||
{ |
|||
to = GetOrCreateContainer(toIndex); |
|||
} |
|||
|
|||
if (PageTransition != null && (from != null || to != null)) |
|||
{ |
|||
await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default); |
|||
} |
|||
else if (to != null) |
|||
{ |
|||
to.IsVisible = true; |
|||
} |
|||
|
|||
if (from != null) |
|||
{ |
|||
if (IsVirtualized) |
|||
{ |
|||
Panel!.Children.Remove(from); |
|||
generator.Dematerialize(fromIndex, 1); |
|||
} |
|||
else |
|||
{ |
|||
from.IsVisible = false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Control? GetOrCreateContainer(int index) |
|||
{ |
|||
var container = ItemContainerGenerator.ContainerFromIndex(index); |
|||
|
|||
if (container == null && IsVirtualized) |
|||
{ |
|||
var item = Items!.Cast<object>().ElementAt(index); |
|||
var materialized = ItemContainerGenerator.Materialize(index, item); |
|||
Panel!.Children.Add(materialized.ContainerControl); |
|||
container = materialized.ContainerControl; |
|||
} |
|||
|
|||
return container; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="IsVirtualized"/> property changes.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (Panel != null) |
|||
{ |
|||
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="SelectedIndex"/> property changes.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (Panel != null) |
|||
{ |
|||
if (_currentTransition == null) |
|||
{ |
|||
int fromIndex = (int)e.OldValue!; |
|||
int toIndex = (int)e.NewValue!; |
|||
|
|||
for (;;) |
|||
{ |
|||
_currentTransition = MoveToPage(fromIndex, toIndex); |
|||
await _currentTransition; |
|||
|
|||
if (_queuedTransitionIndex != -1) |
|||
{ |
|||
fromIndex = toIndex; |
|||
toIndex = _queuedTransitionIndex; |
|||
_queuedTransitionIndex = -1; |
|||
} |
|||
else |
|||
{ |
|||
_currentTransition = null; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_queuedTransitionIndex = (int)e.NewValue!; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
[NotClientImplementable] |
|||
public interface IItemsPresenter : IPresenter |
|||
{ |
|||
IEnumerable? Items { get; set; } |
|||
|
|||
Panel? Panel { get; } |
|||
|
|||
void ItemsChanged(NotifyCollectionChangedEventArgs e); |
|||
|
|||
void ScrollIntoView(int index); |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a control which hosts an items presenter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This interface is implemented by <see cref="ItemsControl"/> which usually contains an
|
|||
/// <see cref="ItemsPresenter"/> and exposes it through its
|
|||
/// <see cref="ItemsControl.Presenter"/> property. ItemsPresenters can be within
|
|||
/// nested templates or in popups and so are not necessarily created immediately when the
|
|||
/// parent control's template is instantiated so they register themselves using this
|
|||
/// interface.
|
|||
/// </remarks>
|
|||
[NotClientImplementable] |
|||
public interface IItemsPresenterHost |
|||
{ |
|||
/// <summary>
|
|||
/// Registers an <see cref="IItemsPresenter"/> with a host control.
|
|||
/// </summary>
|
|||
/// <param name="presenter">The items presenter.</param>
|
|||
void RegisterItemsPresenter(IItemsPresenter presenter); |
|||
} |
|||
} |
|||
@ -1,125 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
internal static class ItemContainerSync |
|||
{ |
|||
public static void ItemsChanged( |
|||
ItemsPresenterBase owner, |
|||
IEnumerable? items, |
|||
NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
var generator = owner.ItemContainerGenerator; |
|||
var panel = owner.Panel; |
|||
|
|||
if (panel == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
void Add() |
|||
{ |
|||
if (e.NewStartingIndex + e.NewItems!.Count < items!.Count()) |
|||
{ |
|||
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); |
|||
} |
|||
|
|||
AddContainers(owner, e.NewStartingIndex, e.NewItems); |
|||
} |
|||
|
|||
void Remove() |
|||
{ |
|||
RemoveContainers(panel, generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count)); |
|||
} |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
Add(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
Remove(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
RemoveContainers(panel, generator.Dematerialize(e.OldStartingIndex, e.OldItems!.Count)); |
|||
var containers = AddContainers(owner, e.NewStartingIndex, e.NewItems!); |
|||
|
|||
var i = e.NewStartingIndex; |
|||
|
|||
foreach (var container in containers) |
|||
{ |
|||
panel.Children[i++] = container.ContainerControl; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Move: |
|||
Remove(); |
|||
Add(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
RemoveContainers(panel, generator.Clear()); |
|||
|
|||
if (items != null) |
|||
{ |
|||
AddContainers(owner, 0, items); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static IList<ItemContainerInfo> AddContainers( |
|||
ItemsPresenterBase owner, |
|||
int index, |
|||
IEnumerable items) |
|||
{ |
|||
var generator = owner.ItemContainerGenerator; |
|||
var result = new List<ItemContainerInfo>(); |
|||
var panel = owner.Panel; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
var i = generator.Materialize(index++, item); |
|||
|
|||
if (i.ContainerControl != null) |
|||
{ |
|||
if (i.Index < panel!.Children.Count) |
|||
{ |
|||
// TODO: This will insert at the wrong place when there are null items.
|
|||
panel.Children.Insert(i.Index, i.ContainerControl); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.Add(i.ContainerControl); |
|||
} |
|||
} |
|||
|
|||
result.Add(i); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static void RemoveContainers( |
|||
Panel panel, |
|||
IEnumerable<ItemContainerInfo> items) |
|||
{ |
|||
foreach (var i in items) |
|||
{ |
|||
if (i.ContainerControl != null) |
|||
{ |
|||
panel.Children.Remove(i.ContainerControl); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,303 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
|
|||
/// </summary>
|
|||
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable |
|||
{ |
|||
private double _crossAxisOffset; |
|||
private IDisposable? _subscriptions; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner"></param>
|
|||
public ItemVirtualizer(ItemsPresenter owner) |
|||
{ |
|||
Owner = owner; |
|||
Items = owner.Items; |
|||
ItemCount = owner.Items.Count(); |
|||
|
|||
var panel = VirtualizingPanel; |
|||
|
|||
if (panel != null) |
|||
{ |
|||
_subscriptions = ((AvaloniaObject)panel).GetObservable(Panel.BoundsProperty) |
|||
.Skip(1) |
|||
.Subscribe(_ => InvalidateScroll()); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ItemsPresenter"/> which owns the virtualizer.
|
|||
/// </summary>
|
|||
public ItemsPresenter Owner { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IVirtualizingPanel"/> which will host the items.
|
|||
/// </summary>
|
|||
public IVirtualizingPanel? VirtualizingPanel => Owner.Panel as IVirtualizingPanel; |
|||
|
|||
/// <summary>
|
|||
/// Gets the items to display.
|
|||
/// </summary>
|
|||
public IEnumerable? Items { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items in <see cref="Items"/>.
|
|||
/// </summary>
|
|||
public int ItemCount { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the index of the first item displayed in the panel.
|
|||
/// </summary>
|
|||
public int FirstIndex { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the index of the first item beyond those displayed in the panel.
|
|||
/// </summary>
|
|||
public int NextIndex { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
|
|||
/// </summary>
|
|||
public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether logical scrolling is enabled.
|
|||
/// </summary>
|
|||
public abstract bool IsLogicalScrollEnabled { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the scroll extent.
|
|||
/// </summary>
|
|||
public abstract double ExtentValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value of the current scroll offset.
|
|||
/// </summary>
|
|||
public abstract double OffsetValue { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the scrollable viewport.
|
|||
/// </summary>
|
|||
public abstract double ViewportValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
|
|||
/// </summary>
|
|||
public Size Extent |
|||
{ |
|||
get |
|||
{ |
|||
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel) |
|||
{ |
|||
return Vertical ? |
|||
new Size(panel.DesiredSize.Width, ExtentValue) : |
|||
new Size(ExtentValue, panel.DesiredSize.Height); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
|
|||
/// </summary>
|
|||
public Size Viewport |
|||
{ |
|||
get |
|||
{ |
|||
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel) |
|||
{ |
|||
return Vertical ? |
|||
new Size(panel.Bounds.Width, ViewportValue) : |
|||
new Size(ViewportValue, panel.Bounds.Height); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
|
|||
/// </summary>
|
|||
public Vector Offset |
|||
{ |
|||
get |
|||
{ |
|||
if (IsLogicalScrollEnabled) |
|||
{ |
|||
return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
if (!IsLogicalScrollEnabled) |
|||
{ |
|||
throw new NotSupportedException("Logical scrolling disabled."); |
|||
} |
|||
|
|||
var oldCrossAxisOffset = _crossAxisOffset; |
|||
|
|||
if (Vertical) |
|||
{ |
|||
OffsetValue = value.Y; |
|||
_crossAxisOffset = value.X; |
|||
} |
|||
else |
|||
{ |
|||
OffsetValue = value.X; |
|||
_crossAxisOffset = value.Y; |
|||
} |
|||
|
|||
if (_crossAxisOffset != oldCrossAxisOffset) |
|||
{ |
|||
Owner.InvalidateArrange(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates an <see cref="ItemVirtualizer"/> based on an item presenter's
|
|||
/// <see cref="ItemVirtualizationMode"/>.
|
|||
/// </summary>
|
|||
/// <param name="owner">The items presenter.</param>
|
|||
/// <returns>An <see cref="ItemVirtualizer"/>.</returns>
|
|||
public static ItemVirtualizer? Create(ItemsPresenter owner) |
|||
{ |
|||
if (owner.Panel == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var virtualizingPanel = owner.Panel as IVirtualizingPanel; |
|||
var scrollContentPresenter = owner.Parent as IScrollable; |
|||
ItemVirtualizer? result = null; |
|||
|
|||
if (virtualizingPanel != null && scrollContentPresenter is object) |
|||
{ |
|||
switch (owner.VirtualizationMode) |
|||
{ |
|||
case ItemVirtualizationMode.Simple: |
|||
result = new ItemVirtualizerSimple(owner); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (result == null) |
|||
{ |
|||
result = new ItemVirtualizerNone(owner); |
|||
} |
|||
|
|||
if (virtualizingPanel != null) |
|||
{ |
|||
virtualizingPanel.Controller = result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Carries out a measure for the related <see cref="ItemsPresenter"/>.
|
|||
/// </summary>
|
|||
/// <param name="availableSize">The size available to the control.</param>
|
|||
/// <returns>The desired size for the control.</returns>
|
|||
public virtual Size MeasureOverride(Size availableSize) |
|||
{ |
|||
Owner.Panel!.Measure(availableSize); |
|||
return Owner.Panel.DesiredSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
|
|||
/// </summary>
|
|||
/// <param name="finalSize">The size available to the control.</param>
|
|||
/// <returns>The actual size used.</returns>
|
|||
public virtual Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
if (VirtualizingPanel != null) |
|||
{ |
|||
VirtualizingPanel.CrossAxisOffset = _crossAxisOffset; |
|||
Owner.Panel!.Arrange(new Rect(finalSize)); |
|||
} |
|||
else |
|||
{ |
|||
var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset); |
|||
Owner.Panel!.Arrange(new Rect(origin, finalSize)); |
|||
} |
|||
|
|||
return finalSize; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void UpdateControls() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the next control in the specified direction.
|
|||
/// </summary>
|
|||
/// <param name="direction">The movement direction.</param>
|
|||
/// <param name="from">The control from which movement begins.</param>
|
|||
/// <returns>The control.</returns>
|
|||
public virtual Control? GetControlInDirection(NavigationDirection direction, Control? from) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the items for the presenter change, either because
|
|||
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
|
|||
/// modified, or the panel has been created.
|
|||
/// </summary>
|
|||
/// <param name="items">The items.</param>
|
|||
/// <param name="e">A description of the change.</param>
|
|||
public virtual void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
Items = items; |
|||
ItemCount = items?.Count() ?? 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the specified item into view.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
public virtual void ScrollIntoView(int index) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void Dispose() |
|||
{ |
|||
_subscriptions?.Dispose(); |
|||
_subscriptions = null; |
|||
|
|||
if (VirtualizingPanel != null) |
|||
{ |
|||
VirtualizingPanel.Controller = null; |
|||
VirtualizingPanel.Children.Clear(); |
|||
} |
|||
|
|||
Owner.ItemContainerGenerator.Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invalidates the current scroll.
|
|||
/// </summary>
|
|||
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty); |
|||
} |
|||
} |
|||
@ -1,106 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an item virtualizer for an <see cref="ItemsPresenter"/> that doesn't actually
|
|||
/// virtualize items - it just creates a container for every item.
|
|||
/// </summary>
|
|||
internal class ItemVirtualizerNone : ItemVirtualizer |
|||
{ |
|||
public ItemVirtualizerNone(ItemsPresenter owner) |
|||
: base(owner) |
|||
{ |
|||
if (Items != null && owner.Panel != null) |
|||
{ |
|||
AddContainers(0, Items); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool IsLogicalScrollEnabled => false; |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double ExtentValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double OffsetValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
set { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double ViewportValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ItemsChanged(items, e); |
|||
ItemContainerSync.ItemsChanged(Owner, items, e); |
|||
Owner.InvalidateMeasure(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the specified item into view.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
public override void ScrollIntoView(int index) |
|||
{ |
|||
if (index != -1) |
|||
{ |
|||
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); |
|||
container?.BringIntoView(); |
|||
} |
|||
} |
|||
|
|||
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items) |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var result = new List<ItemContainerInfo>(); |
|||
var panel = Owner.Panel; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
var i = generator.Materialize(index++, item); |
|||
|
|||
if (i.ContainerControl != null) |
|||
{ |
|||
if (i.Index < panel!.Children.Count) |
|||
{ |
|||
// TODO: This will insert at the wrong place when there are null items.
|
|||
panel.Children.Insert(i.Index, i.ContainerControl); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.Add(i.ContainerControl); |
|||
} |
|||
} |
|||
|
|||
result.Add(i); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -1,606 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
|
|||
/// <see cref="ItemVirtualizationMode.Simple"/>.
|
|||
/// </summary>
|
|||
internal class ItemVirtualizerSimple : ItemVirtualizer |
|||
{ |
|||
private int _anchor; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner"></param>
|
|||
public ItemVirtualizerSimple(ItemsPresenter owner) |
|||
: base(owner) |
|||
{ |
|||
// Don't need to add children here as UpdateControls should be called by the panel
|
|||
// measure/arrange.
|
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool IsLogicalScrollEnabled => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double ExtentValue => ItemCount; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double OffsetValue |
|||
{ |
|||
get |
|||
{ |
|||
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; |
|||
return FirstIndex + offset; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; |
|||
var delta = (int)(value - (FirstIndex + offset)); |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
var newLastIndex = (NextIndex - 1) + delta; |
|||
|
|||
if (newLastIndex < ItemCount) |
|||
{ |
|||
if (panel.PixelOffset > 0) |
|||
{ |
|||
panel.PixelOffset = 0; |
|||
delta += 1; |
|||
} |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
RecycleContainersForMove(delta); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// We're moving to a partially obscured item at the end of the list so
|
|||
// offset the panel by the height of the first item.
|
|||
var firstIndex = ItemCount - panel.Children.Count; |
|||
RecycleContainersForMove(firstIndex - FirstIndex); |
|||
|
|||
double pixelOffset; |
|||
var child = panel.Children[0]; |
|||
|
|||
if (child.IsArrangeValid) |
|||
{ |
|||
pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? |
|||
child.Bounds.Height : |
|||
child.Bounds.Width; |
|||
} |
|||
else |
|||
{ |
|||
pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? |
|||
child.DesiredSize.Height : |
|||
child.DesiredSize.Width; |
|||
} |
|||
|
|||
panel.PixelOffset = pixelOffset; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double ViewportValue |
|||
{ |
|||
get |
|||
{ |
|||
// If we can't fit the last item in the panel fully, subtract 1 from the viewport.
|
|||
var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; |
|||
return VirtualizingPanel.Children.Count - overflow; |
|||
} |
|||
} |
|||
|
|||
public new IVirtualizingPanel VirtualizingPanel => base.VirtualizingPanel!; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
var scrollable = (ILogicalScrollable)Owner; |
|||
var visualRoot = Owner.GetVisualRoot(); |
|||
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint |
|||
?? (visualRoot as TopLevel)?.ClientSize; |
|||
|
|||
// If infinity is passed as the available size and we're virtualized then we need to
|
|||
// fill the available space, but to do that we *don't* want to materialize all our
|
|||
// items! Take a look at the root of the tree for a MaxClientSize and use that as
|
|||
// the available size.
|
|||
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) |
|||
{ |
|||
if (availableSize.Height == double.PositiveInfinity) |
|||
{ |
|||
if (maxAvailableSize.HasValue) |
|||
{ |
|||
availableSize = availableSize.WithHeight(maxAvailableSize.Value.Height); |
|||
} |
|||
} |
|||
|
|||
if (scrollable.CanHorizontallyScroll) |
|||
{ |
|||
availableSize = availableSize.WithWidth(double.PositiveInfinity); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (availableSize.Width == double.PositiveInfinity) |
|||
{ |
|||
if (maxAvailableSize.HasValue) |
|||
{ |
|||
availableSize = availableSize.WithWidth(maxAvailableSize.Value.Width); |
|||
} |
|||
} |
|||
|
|||
if (scrollable.CanVerticallyScroll) |
|||
{ |
|||
availableSize = availableSize.WithHeight(double.PositiveInfinity); |
|||
} |
|||
} |
|||
|
|||
Owner.Panel!.Measure(availableSize); |
|||
return Owner.Panel.DesiredSize; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void UpdateControls() |
|||
{ |
|||
CreateAndRemoveContainers(); |
|||
InvalidateScroll(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ItemsChanged(items, e); |
|||
|
|||
var panel = VirtualizingPanel; |
|||
|
|||
if (items != null) |
|||
{ |
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
CreateAndRemoveContainers(); |
|||
|
|||
if (e.NewStartingIndex < NextIndex) |
|||
{ |
|||
RecycleContainers(); |
|||
} |
|||
|
|||
panel.ForceInvalidateMeasure(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
if (e.OldStartingIndex < NextIndex || |
|||
panel.Children.Count > ItemCount) |
|||
{ |
|||
RecycleContainersOnRemove(); |
|||
} |
|||
|
|||
panel.ForceInvalidateMeasure(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Move: |
|||
case NotifyCollectionChangedAction.Replace: |
|||
RecycleContainers(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
RecycleContainersOnRemove(); |
|||
CreateAndRemoveContainers(); |
|||
panel.ForceInvalidateMeasure(); |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Owner.ItemContainerGenerator.Clear(); |
|||
VirtualizingPanel.Children.Clear(); |
|||
FirstIndex = NextIndex = 0; |
|||
} |
|||
|
|||
// If we are scrolled to view a partially visible last item but controls were added
|
|||
// then we need to return to a non-offset scroll position.
|
|||
if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount) |
|||
{ |
|||
panel.PixelOffset = 0; |
|||
RecycleContainersForMove(1); |
|||
} |
|||
|
|||
InvalidateScroll(); |
|||
} |
|||
|
|||
public override Control? GetControlInDirection(NavigationDirection direction, Control? from) |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var panel = VirtualizingPanel; |
|||
var itemIndex = generator.IndexFromContainer(from); |
|||
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; |
|||
|
|||
var newItemIndex = -1; |
|||
|
|||
switch (direction) |
|||
{ |
|||
case NavigationDirection.First: |
|||
newItemIndex = 0; |
|||
break; |
|||
|
|||
case NavigationDirection.Last: |
|||
newItemIndex = ItemCount - 1; |
|||
break; |
|||
|
|||
default: |
|||
if (itemIndex == -1) |
|||
{ |
|||
return null; |
|||
} |
|||
break; |
|||
} |
|||
|
|||
switch (direction) |
|||
{ |
|||
case NavigationDirection.Up: |
|||
if (vertical) |
|||
{ |
|||
newItemIndex = itemIndex - 1; |
|||
} |
|||
|
|||
break; |
|||
case NavigationDirection.Down: |
|||
if (vertical) |
|||
{ |
|||
newItemIndex = itemIndex + 1; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NavigationDirection.Left: |
|||
if (!vertical) |
|||
{ |
|||
newItemIndex = itemIndex - 1; |
|||
} |
|||
break; |
|||
|
|||
case NavigationDirection.Right: |
|||
if (!vertical) |
|||
{ |
|||
newItemIndex = itemIndex + 1; |
|||
} |
|||
break; |
|||
|
|||
case NavigationDirection.PageUp: |
|||
newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); |
|||
break; |
|||
|
|||
case NavigationDirection.PageDown: |
|||
newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); |
|||
break; |
|||
} |
|||
|
|||
return ScrollIntoViewCore(newItemIndex); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ScrollIntoView(int index) |
|||
{ |
|||
if (index != -1) |
|||
{ |
|||
ScrollIntoViewCore(index); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates and removes containers such that we have at most enough containers to fill
|
|||
/// the panel.
|
|||
/// </summary>
|
|||
private void CreateAndRemoveContainers() |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var panel = VirtualizingPanel; |
|||
var panelControl = (Control)panel; |
|||
|
|||
if (!panel.IsFull && Items != null && panelControl.IsAttachedToVisualTree) |
|||
{ |
|||
var index = NextIndex; |
|||
var step = 1; |
|||
|
|||
while (!panel.IsFull && index >= 0) |
|||
{ |
|||
if (index >= ItemCount) |
|||
{ |
|||
// We can fit more containers in the panel, but we're at the end of the
|
|||
// items. If we're scrolled to the top (FirstIndex == 0), then there are
|
|||
// no more items to create. Otherwise, go backwards adding containers to
|
|||
// the beginning of the panel.
|
|||
if (FirstIndex == 0) |
|||
{ |
|||
break; |
|||
} |
|||
else |
|||
{ |
|||
index = FirstIndex - 1; |
|||
step = -1; |
|||
} |
|||
} |
|||
|
|||
var materialized = generator.Materialize(index, Items.ElementAt(index)!); |
|||
|
|||
if (step == 1) |
|||
{ |
|||
panel.Children.Add(materialized.ContainerControl); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.Insert(0, materialized.ContainerControl); |
|||
} |
|||
|
|||
index += step; |
|||
} |
|||
|
|||
if (step == 1) |
|||
{ |
|||
NextIndex = index; |
|||
} |
|||
else |
|||
{ |
|||
NextIndex = ItemCount; |
|||
FirstIndex = index + 1; |
|||
} |
|||
} |
|||
|
|||
if (panel.OverflowCount > 0) |
|||
{ |
|||
if (_anchor <= FirstIndex) |
|||
{ |
|||
RemoveContainers(panel.OverflowCount); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the containers in the panel to make sure they are displaying the correct item
|
|||
/// based on <see cref="ItemVirtualizer.FirstIndex"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
|
|||
/// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
|
|||
/// </remarks>
|
|||
private void RecycleContainers() |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var containers = generator.Containers.ToList(); |
|||
var itemIndex = FirstIndex; |
|||
|
|||
foreach (var container in containers) |
|||
{ |
|||
var item = Items!.ElementAt(itemIndex)!; |
|||
|
|||
if (!object.Equals(container.Item, item)) |
|||
{ |
|||
if (!generator.TryRecycle(itemIndex, itemIndex, item)) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
++itemIndex; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles containers when a move occurs.
|
|||
/// </summary>
|
|||
/// <param name="delta">The delta of the move.</param>
|
|||
/// <remarks>
|
|||
/// If the move is less than a page, then this method moves the containers for the items
|
|||
/// that are still visible to the correct place, and recycles and moves the others. For
|
|||
/// example: if there are 20 items and 10 containers visible and the user scrolls 5
|
|||
/// items down, then the bottom 5 containers will be moved to the top and the top 5 will
|
|||
/// be moved to the bottom and recycled to display the newly visible item. Updates
|
|||
/// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
|
|||
/// with their new values.
|
|||
/// </remarks>
|
|||
private void RecycleContainersForMove(int delta) |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
|
|||
//validate delta it should never overflow last index or generate index < 0
|
|||
delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); |
|||
|
|||
var sign = delta < 0 ? -1 : 1; |
|||
var count = Math.Min(Math.Abs(delta), panel.Children.Count); |
|||
var move = count < panel.Children.Count; |
|||
var first = delta < 0 && move ? panel.Children.Count + delta : 0; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var oldItemIndex = FirstIndex + first + i; |
|||
var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); |
|||
|
|||
var item = Items!.ElementAt(newItemIndex)!; |
|||
|
|||
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
if (move) |
|||
{ |
|||
if (delta > 0) |
|||
{ |
|||
panel.Children.MoveRange(first, count, panel.Children.Count); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.MoveRange(first, count, 0); |
|||
} |
|||
} |
|||
|
|||
FirstIndex += delta; |
|||
NextIndex += delta; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles containers due to items being removed.
|
|||
/// </summary>
|
|||
private void RecycleContainersOnRemove() |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
|
|||
if (NextIndex <= ItemCount) |
|||
{ |
|||
// Items have been removed but FirstIndex..NextIndex is still a valid range in the
|
|||
// items, so just recycle the containers to adapt to the new state.
|
|||
RecycleContainers(); |
|||
} |
|||
else |
|||
{ |
|||
// Items have been removed and now the range FirstIndex..NextIndex goes out of
|
|||
// the item bounds. Remove any excess containers, try to scroll up and then recycle
|
|||
// the containers to make sure they point to the correct item.
|
|||
var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); |
|||
var delta = newFirstIndex - FirstIndex; |
|||
var newNextIndex = NextIndex + delta; |
|||
|
|||
if (newNextIndex > ItemCount) |
|||
{ |
|||
RemoveContainers(newNextIndex - ItemCount); |
|||
} |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
RecycleContainersForMove(delta); |
|||
} |
|||
|
|||
RecycleContainers(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the specified number of containers from the end of the panel and updates
|
|||
/// <see cref="ItemVirtualizer.NextIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="count">The number of containers to remove.</param>
|
|||
private void RemoveContainers(int count) |
|||
{ |
|||
var index = VirtualizingPanel.Children.Count - count; |
|||
|
|||
VirtualizingPanel.Children.RemoveRange(index, count); |
|||
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); |
|||
NextIndex -= count; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the item with the specified index into view.
|
|||
/// </summary>
|
|||
/// <param name="index">The item index.</param>
|
|||
/// <returns>The container that was brought into view.</returns>
|
|||
private Control? ScrollIntoViewCore(int index) |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var panelControl = (Control)panel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var newOffset = -1.0; |
|||
|
|||
//better not trigger any container generation/recycle while or layout stuff
|
|||
//before panel is attached/visible
|
|||
if (!panelControl.IsAttachedToVisualTree) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (!panelControl.IsMeasureValid && panelControl.PreviousMeasure.HasValue) |
|||
{ |
|||
//before any kind of scrolling we need to make sure panel measure is valid
|
|||
//or we risk get panel into not valid state
|
|||
//we make a preemptive quick measure so scrolling is valid
|
|||
panelControl.Measure(panelControl.PreviousMeasure.Value); |
|||
} |
|||
|
|||
if (index >= 0 && index < ItemCount) |
|||
{ |
|||
if (index <= FirstIndex) |
|||
{ |
|||
newOffset = index; |
|||
} |
|||
else if (index >= NextIndex) |
|||
{ |
|||
newOffset = index - Math.Ceiling(ViewportValue - 1); |
|||
} |
|||
|
|||
if (newOffset != -1) |
|||
{ |
|||
OffsetValue = newOffset; |
|||
} |
|||
|
|||
var container = generator.ContainerFromIndex(index); |
|||
var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager; |
|||
|
|||
// We need to do a layout here because it's possible that the container we moved to
|
|||
// is only partially visible due to differing item sizes. If the container is only
|
|||
// partially visible, scroll again. Don't do this if there's no layout manager:
|
|||
// it means we're running a unit test.
|
|||
if (container != null && layoutManager != null) |
|||
{ |
|||
_anchor = index; |
|||
layoutManager.ExecuteLayoutPass(); |
|||
_anchor = -1; |
|||
|
|||
if (newOffset != -1 && newOffset != OffsetValue) |
|||
{ |
|||
OffsetValue = newOffset; |
|||
} |
|||
|
|||
if (panel.ScrollDirection == Orientation.Vertical) |
|||
{ |
|||
if (container.Bounds.Y < panelControl.Bounds.Y || container.Bounds.Bottom > panelControl.Bounds.Bottom) |
|||
{ |
|||
OffsetValue += 1; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (container.Bounds.X < panelControl.Bounds.X || container.Bounds.Right > panelControl.Bounds.Right) |
|||
{ |
|||
OffsetValue += 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return container; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ensures an offset value is within the value range.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
/// <returns>The coerced value.</returns>
|
|||
private double CoerceOffset(double value) |
|||
{ |
|||
var max = Math.Max(ExtentValue - ViewportValue, 0); |
|||
return MathUtilities.Clamp(value, 0, max); |
|||
} |
|||
} |
|||
} |
|||
@ -1,179 +1,208 @@ |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using static Avalonia.Utilities.MathUtilities; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Displays items inside an <see cref="ItemsControl"/>.
|
|||
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
|
|||
/// </summary>
|
|||
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable |
|||
public class ItemsPresenter : Control, ILogicalScrollable |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="VirtualizationMode"/> property.
|
|||
/// Defines the <see cref="ItemsPanel"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty = |
|||
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>( |
|||
nameof(VirtualizationMode), |
|||
defaultValue: ItemVirtualizationMode.None); |
|||
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty = |
|||
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>(); |
|||
|
|||
private bool _canHorizontallyScroll; |
|||
private bool _canVerticallyScroll; |
|||
private PanelContainerGenerator? _generator; |
|||
private ILogicalScrollable? _logicalScrollable; |
|||
private EventHandler? _scrollInvalidated; |
|||
|
|||
/// <summary>
|
|||
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
|
|||
/// </summary>
|
|||
static ItemsPresenter() |
|||
{ |
|||
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( |
|||
typeof(ItemsPresenter), |
|||
KeyboardNavigationMode.Once); |
|||
} |
|||
|
|||
VirtualizationModeProperty.Changed |
|||
.AddClassHandler<ItemsPresenter>((x, e) => x.VirtualizationModeChanged(e)); |
|||
event EventHandler? ILogicalScrollable.ScrollInvalidated |
|||
{ |
|||
add => _scrollInvalidated += value; |
|||
remove => _scrollInvalidated -= value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the virtualization mode for the items.
|
|||
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
|
|||
/// </summary>
|
|||
public ItemVirtualizationMode VirtualizationMode |
|||
public ITemplate<Panel> ItemsPanel |
|||
{ |
|||
get { return GetValue(VirtualizationModeProperty); } |
|||
set { SetValue(VirtualizationModeProperty, value); } |
|||
get => GetValue(ItemsPanelProperty); |
|||
set => SetValue(ItemsPanelProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// Gets the panel used to display the items.
|
|||
/// </summary>
|
|||
bool ILogicalScrollable.CanHorizontallyScroll |
|||
public Panel? Panel { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the owner <see cref="ItemsControl"/>.
|
|||
/// </summary>
|
|||
internal ItemsControl? ItemsControl { get; private set; } |
|||
|
|||
bool ILogicalScrollable.CanHorizontallyScroll |
|||
{ |
|||
get { return _canHorizontallyScroll; } |
|||
get => _logicalScrollable?.CanHorizontallyScroll ?? false; |
|||
set |
|||
{ |
|||
_canHorizontallyScroll = value; |
|||
InvalidateMeasure(); |
|||
if (_logicalScrollable is not null) |
|||
_logicalScrollable.CanHorizontallyScroll = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// </summary>
|
|||
bool ILogicalScrollable.CanVerticallyScroll |
|||
bool ILogicalScrollable.CanVerticallyScroll |
|||
{ |
|||
get { return _canVerticallyScroll; } |
|||
get => _logicalScrollable?.CanVerticallyScroll ?? false; |
|||
set |
|||
{ |
|||
_canVerticallyScroll = value; |
|||
InvalidateMeasure(); |
|||
if (_logicalScrollable is not null) |
|||
_logicalScrollable.CanVerticallyScroll = value; |
|||
} |
|||
} |
|||
/// <inheritdoc/>
|
|||
bool ILogicalScrollable.IsLogicalScrollEnabled |
|||
{ |
|||
get { return Virtualizer?.IsLogicalScrollEnabled ?? false; } |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
Size IScrollable.Extent => Virtualizer?.Extent ?? default; |
|||
|
|||
/// <inheritdoc/>
|
|||
Vector IScrollable.Offset |
|||
Vector IScrollable.Offset |
|||
{ |
|||
get { return Virtualizer?.Offset ?? new Vector(); } |
|||
get => _logicalScrollable?.Offset ?? default; |
|||
set |
|||
{ |
|||
if (Virtualizer != null) |
|||
{ |
|||
Virtualizer.Offset = CoerceOffset(value); |
|||
} |
|||
if (_logicalScrollable is not null) |
|||
_logicalScrollable.Offset = value; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size; |
|||
bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; |
|||
Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; |
|||
Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; |
|||
Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; |
|||
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; |
|||
|
|||
/// <inheritdoc/>
|
|||
event EventHandler? ILogicalScrollable.ScrollInvalidated |
|||
public override sealed void ApplyTemplate() |
|||
{ |
|||
add => _scrollInvalidated += value; |
|||
remove => _scrollInvalidated -= value; |
|||
} |
|||
if (Panel is null && ItemsControl is not null) |
|||
{ |
|||
if (_logicalScrollable is not null) |
|||
{ |
|||
_logicalScrollable.ScrollInvalidated -= OnLogicalScrollInvalidated; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
Size ILogicalScrollable.ScrollSize => new Size(ScrollViewer.DefaultSmallChange, 1); |
|||
Panel = ItemsPanel.Build(); |
|||
Panel.SetValue(TemplatedParentProperty, TemplatedParent); |
|||
LogicalChildren.Add(Panel); |
|||
VisualChildren.Add(Panel); |
|||
|
|||
/// <inheritdoc/>
|
|||
Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16); |
|||
if (Panel is VirtualizingPanel v) |
|||
v.Attach(ItemsControl); |
|||
else |
|||
CreateSimplePanelGenerator(); |
|||
|
|||
internal ItemVirtualizer? Virtualizer { get; private set; } |
|||
_logicalScrollable = Panel as ILogicalScrollable; |
|||
|
|||
/// <inheritdoc/>
|
|||
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) |
|||
{ |
|||
return false; |
|||
if (_logicalScrollable is not null) |
|||
{ |
|||
_logicalScrollable.ScrollInvalidated += OnLogicalScrollInvalidated; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) |
|||
{ |
|||
return Virtualizer?.GetControlInDirection(direction, from); |
|||
} |
|||
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) => |
|||
_logicalScrollable?.BringIntoView(target, targetRect) ?? false; |
|||
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) => |
|||
_logicalScrollable?.GetControlInDirection(direction, from); |
|||
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e); |
|||
|
|||
/// <inheritdoc/>
|
|||
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) |
|||
internal void ScrollIntoView(int index) |
|||
{ |
|||
_scrollInvalidated?.Invoke(this, e); |
|||
if (Panel is VirtualizingPanel v) |
|||
v.ScrollIntoView(index); |
|||
else if (index >= 0 && index < Panel?.Children.Count) |
|||
Panel.Children[index].BringIntoView(); |
|||
} |
|||
|
|||
public override void ScrollIntoView(int index) |
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
Virtualizer?.ScrollIntoView(index); |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == TemplatedParentProperty) |
|||
{ |
|||
ResetState(); |
|||
ItemsControl = null; |
|||
|
|||
if (change.NewValue is ItemsControl itemsControl) |
|||
{ |
|||
ItemsControl = itemsControl; |
|||
ItemsControl.RegisterItemsPresenter(this); |
|||
} |
|||
} |
|||
else if (change.Property == ItemsPanelProperty) |
|||
{ |
|||
ResetState(); |
|||
InvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
internal void Refresh() |
|||
{ |
|||
return Virtualizer?.MeasureOverride(availableSize) ?? default; |
|||
if (Panel is VirtualizingPanel v) |
|||
v.Refresh(); |
|||
else |
|||
_generator?.Refresh(); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
private void ResetState() |
|||
{ |
|||
return Virtualizer?.ArrangeOverride(finalSize) ?? default; |
|||
_generator?.Dispose(); |
|||
_generator = null; |
|||
LogicalChildren.Clear(); |
|||
VisualChildren.Clear(); |
|||
(Panel as VirtualizingPanel)?.Detach(); |
|||
Panel = null; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void PanelCreated(Panel panel) |
|||
private void CreateSimplePanelGenerator() |
|||
{ |
|||
Virtualizer?.Dispose(); |
|||
Virtualizer = ItemVirtualizer.Create(this); |
|||
_scrollInvalidated?.Invoke(this, EventArgs.Empty); |
|||
Debug.Assert(Panel is not VirtualizingPanel); |
|||
|
|||
KeyboardNavigation.SetTabNavigation( |
|||
(InputElement)panel, |
|||
KeyboardNavigation.GetTabNavigation(this)); |
|||
if (ItemsControl is null || Panel is null) |
|||
return; |
|||
|
|||
_generator?.Dispose(); |
|||
_generator = new(this); |
|||
} |
|||
|
|||
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) |
|||
internal Control? ContainerFromIndex(int index) |
|||
{ |
|||
Virtualizer?.ItemsChanged(Items, e); |
|||
if (Panel is VirtualizingPanel v) |
|||
return v.ContainerFromIndex(index); |
|||
return index >= 0 && index < Panel?.Children.Count ? Panel.Children[index] : null; |
|||
} |
|||
|
|||
private Vector CoerceOffset(Vector value) |
|||
internal IEnumerable<Control>? GetRealizedContainers() |
|||
{ |
|||
var scrollable = (ILogicalScrollable)this; |
|||
var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); |
|||
var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); |
|||
return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); |
|||
if (Panel is VirtualizingPanel v) |
|||
return v.GetRealizedContainers(); |
|||
return Panel?.Children; |
|||
} |
|||
|
|||
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) |
|||
internal int IndexFromContainer(Control container) |
|||
{ |
|||
Virtualizer?.Dispose(); |
|||
Virtualizer = ItemVirtualizer.Create(this); |
|||
_scrollInvalidated?.Invoke(this, EventArgs.Empty); |
|||
if (Panel is VirtualizingPanel v) |
|||
return v.IndexFromContainer(container); |
|||
return Panel?.Children.IndexOf(container) ?? -1; |
|||
} |
|||
|
|||
private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); |
|||
} |
|||
} |
|||
|
|||
@ -1,308 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
|
|||
/// </summary>
|
|||
public abstract class ItemsPresenterBase : Control, IItemsPresenter, IChildIndexProvider |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Items"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ItemsPresenterBase, IEnumerable?> ItemsProperty = |
|||
ItemsControl.ItemsProperty.AddOwner<ItemsPresenterBase>(o => o.Items, (o, v) => o.Items = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemsPanel"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty = |
|||
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenterBase>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemTemplate"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = |
|||
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="DisplayMemberBinding" /> property
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = |
|||
ItemsControl.DisplayMemberBindingProperty.AddOwner<ItemsPresenterBase>(); |
|||
|
|||
private IEnumerable? _items; |
|||
private IDisposable? _itemsSubscription; |
|||
private bool _createdPanel; |
|||
private IItemContainerGenerator? _generator; |
|||
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged; |
|||
|
|||
/// <summary>
|
|||
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
|
|||
/// </summary>
|
|||
static ItemsPresenterBase() |
|||
{ |
|||
TemplatedParentProperty.Changed.AddClassHandler<ItemsPresenterBase>((x,e) => x.TemplatedParentChanged(e)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the items to be displayed.
|
|||
/// </summary>
|
|||
public IEnumerable? Items |
|||
{ |
|||
get |
|||
{ |
|||
return _items; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_itemsSubscription?.Dispose(); |
|||
_itemsSubscription = null; |
|||
|
|||
if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) |
|||
{ |
|||
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); |
|||
} |
|||
|
|||
SetAndRaise(ItemsProperty, ref _items, value); |
|||
|
|||
if (_createdPanel) |
|||
{ |
|||
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the item container generator.
|
|||
/// </summary>
|
|||
public IItemContainerGenerator ItemContainerGenerator |
|||
{ |
|||
get |
|||
{ |
|||
if (_generator == null) |
|||
{ |
|||
_generator = CreateItemContainerGenerator(); |
|||
} |
|||
|
|||
return _generator; |
|||
} |
|||
|
|||
internal set |
|||
{ |
|||
if (_generator != null) |
|||
{ |
|||
throw new InvalidOperationException("ItemContainerGenerator already created."); |
|||
} |
|||
|
|||
_generator = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
|
|||
/// </summary>
|
|||
public ITemplate<Panel> ItemsPanel |
|||
{ |
|||
get { return GetValue(ItemsPanelProperty); } |
|||
set { SetValue(ItemsPanelProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the data template used to display the items in the control.
|
|||
/// </summary>
|
|||
public IDataTemplate? ItemTemplate |
|||
{ |
|||
get { return GetValue(ItemTemplateProperty); } |
|||
set { SetValue(ItemTemplateProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
|
|||
/// </summary>
|
|||
public IBinding? DisplayMemberBinding |
|||
{ |
|||
get { return GetValue(DisplayMemberBindingProperty); } |
|||
set { SetValue(DisplayMemberBindingProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the panel used to display the items.
|
|||
/// </summary>
|
|||
public Panel? Panel |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
protected bool IsHosted => TemplatedParent is IItemsPresenterHost; |
|||
|
|||
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged |
|||
{ |
|||
add => _childIndexChanged += value; |
|||
remove => _childIndexChanged -= value; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override sealed void ApplyTemplate() |
|||
{ |
|||
if (!_createdPanel) |
|||
{ |
|||
CreatePanel(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void ScrollIntoView(int index) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (Panel != null) |
|||
{ |
|||
ItemsChanged(e); |
|||
|
|||
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// An <see cref="IItemContainerGenerator"/> or null.
|
|||
/// </returns>
|
|||
protected virtual IItemContainerGenerator CreateItemContainerGenerator() |
|||
{ |
|||
var i = TemplatedParent as ItemsControl; |
|||
var result = i?.ItemContainerGenerator; |
|||
|
|||
if (result == null) |
|||
{ |
|||
result = new ItemContainerGenerator(this); |
|||
result.ItemTemplate = ItemTemplate; |
|||
result.DisplayMemberBinding = DisplayMemberBinding; |
|||
} |
|||
|
|||
result.Materialized += ContainerActionHandler; |
|||
result.Dematerialized += ContainerActionHandler; |
|||
result.Recycled += ContainerActionHandler; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private void ContainerActionHandler(object? sender, ItemContainerEventArgs e) |
|||
{ |
|||
for (var i = 0; i < e.Containers.Count; i++) |
|||
{ |
|||
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
Panel!.Measure(availableSize); |
|||
return Panel.DesiredSize; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
Panel!.Arrange(new Rect(finalSize)); |
|||
return finalSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Panel"/> is created.
|
|||
/// </summary>
|
|||
/// <param name="panel">The panel.</param>
|
|||
protected virtual void PanelCreated(Panel panel) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the items for the presenter change, either because <see cref="Items"/>
|
|||
/// has been set, the items collection has been modified, or the panel has been created.
|
|||
/// </summary>
|
|||
/// <param name="e">A description of the change.</param>
|
|||
/// <remarks>
|
|||
/// The panel is guaranteed to be created when this method is called.
|
|||
/// </remarks>
|
|||
protected virtual void ItemsChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
ItemContainerSync.ItemsChanged(this, Items, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
|
|||
/// time.
|
|||
/// </summary>
|
|||
private void CreatePanel() |
|||
{ |
|||
Panel = ItemsPanel.Build(); |
|||
Panel.SetValue(TemplatedParentProperty, TemplatedParent); |
|||
|
|||
LogicalChildren.Clear(); |
|||
VisualChildren.Clear(); |
|||
LogicalChildren.Add(Panel); |
|||
VisualChildren.Add(Panel); |
|||
|
|||
_createdPanel = true; |
|||
|
|||
if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) |
|||
{ |
|||
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); |
|||
} |
|||
|
|||
PanelCreated(Panel); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Items"/> collection changes.
|
|||
/// </summary>
|
|||
/// <param name="sender">The sender.</param>
|
|||
/// <param name="e">The event args.</param>
|
|||
private void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_createdPanel) |
|||
{ |
|||
ItemsChanged(e); |
|||
} |
|||
} |
|||
|
|||
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); |
|||
} |
|||
|
|||
int IChildIndexProvider.GetChildIndex(ILogical child) |
|||
{ |
|||
if (child is Control control && ItemContainerGenerator is { } generator) |
|||
{ |
|||
var index = ItemContainerGenerator.IndexFromContainer(control); |
|||
|
|||
return index; |
|||
} |
|||
|
|||
return -1; |
|||
} |
|||
|
|||
bool IChildIndexProvider.TryGetTotalCount(out int count) |
|||
{ |
|||
return Items.TryGetCountFast(out count); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Generates containers for <see cref="ItemsPresenter"/>s that have non-virtualizing panels.
|
|||
/// </summary>
|
|||
internal class PanelContainerGenerator : IDisposable |
|||
{ |
|||
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty = |
|||
AvaloniaProperty.RegisterAttached<PanelContainerGenerator, Control, bool>("ItemIsOwnContainer"); |
|||
|
|||
private readonly ItemsPresenter _presenter; |
|||
|
|||
public PanelContainerGenerator(ItemsPresenter presenter) |
|||
{ |
|||
Debug.Assert(presenter.ItemsControl is not null); |
|||
Debug.Assert(presenter.Panel is not null or VirtualizingPanel); |
|||
|
|||
_presenter = presenter; |
|||
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged; |
|||
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged; |
|||
|
|||
OnItemsChanged(null, CollectionUtils.ResetEventArgs); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_presenter.ItemsControl is { } itemsControl) |
|||
{ |
|||
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged; |
|||
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged; |
|||
|
|||
ClearItemsControlLogicalChildren(); |
|||
} |
|||
|
|||
_presenter.Panel?.Children.Clear(); |
|||
} |
|||
|
|||
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs); |
|||
|
|||
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Property == ItemsControl.ItemsProperty) |
|||
{ |
|||
if (e.OldValue is INotifyCollectionChanged inccOld) |
|||
inccOld.CollectionChanged -= OnItemsChanged; |
|||
OnItemsChanged(null, CollectionUtils.ResetEventArgs); |
|||
if (e.NewValue is INotifyCollectionChanged inccNew) |
|||
inccNew.CollectionChanged += OnItemsChanged; |
|||
} |
|||
} |
|||
|
|||
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_presenter.Panel is null || _presenter.ItemsControl is null) |
|||
return; |
|||
|
|||
var itemsControl = _presenter.ItemsControl; |
|||
var generator = itemsControl.ItemContainerGenerator; |
|||
var children = _presenter.Panel.Children; |
|||
|
|||
void Add(int index, IEnumerable items) |
|||
{ |
|||
var i = index; |
|||
foreach (var item in items) |
|||
InsertContainer(itemsControl, children, item, i++); |
|||
|
|||
var childCount = children.Count; |
|||
var delta = i - index; |
|||
|
|||
for (; i < childCount; ++i) |
|||
generator.ItemContainerIndexChanged(children[i], i - delta, i); |
|||
} |
|||
|
|||
void Remove(int index, int count) |
|||
{ |
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var c = children[index + i]; |
|||
if (!c.IsSet(ItemIsOwnContainerProperty)) |
|||
itemsControl.RemoveLogicalChild(children[i + index]); |
|||
else |
|||
generator.ClearItemContainer(c); |
|||
} |
|||
|
|||
children.RemoveRange(index, count); |
|||
|
|||
var childCount = children.Count; |
|||
|
|||
for (var i = index; i < childCount; ++i) |
|||
generator.ItemContainerIndexChanged(children[i], i + count, i); |
|||
} |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
Add(e.NewStartingIndex, e.NewItems!); |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
Remove(e.OldStartingIndex, e.OldItems!.Count); |
|||
break; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
case NotifyCollectionChangedAction.Move: |
|||
Remove(e.OldStartingIndex, e.OldItems!.Count); |
|||
Add(e.NewStartingIndex, e.NewItems!); |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
ClearItemsControlLogicalChildren(); |
|||
children.Clear(); |
|||
Add(0, _presenter.ItemsControl.ItemsView); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static void InsertContainer( |
|||
ItemsControl itemsControl, |
|||
Controls children, |
|||
object? item, |
|||
int index) |
|||
{ |
|||
var generator = itemsControl.ItemContainerGenerator; |
|||
Control container; |
|||
|
|||
if (item is Control c && generator.IsItemItsOwnContainer(c)) |
|||
{ |
|||
container = c; |
|||
container.SetValue(ItemIsOwnContainerProperty, true); |
|||
} |
|||
else |
|||
{ |
|||
container = generator.CreateContainer(); |
|||
} |
|||
|
|||
generator.PrepareItemContainer(container, item, index); |
|||
itemsControl.AddLogicalChild(container); |
|||
children.Insert(index, container); |
|||
generator.ItemContainerPrepared(container, item, index); |
|||
} |
|||
|
|||
private void ClearItemsControlLogicalChildren() |
|||
{ |
|||
if (_presenter.Panel is null || _presenter.ItemsControl is null) |
|||
return; |
|||
|
|||
var itemsControl = _presenter.ItemsControl; |
|||
var panel = _presenter.Panel; |
|||
|
|||
foreach (var c in panel.Children) |
|||
{ |
|||
if (!c.IsSet(ItemIsOwnContainerProperty)) |
|||
itemsControl.RemoveLogicalChild(c); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue