/* Based on VirtualizingWrapPanel created by S. Bäumlisberger licensed under MIT license.
https://github.com/sbaeumlisberger/VirtualizingWrapPanel
Copyright (C) S. Bäumlisberger
All Rights Reserved. */
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
// ReSharper disable once CheckNamespace
namespace WPFluent.Controls;
///
/// Base abstract class for creating virtualized panels. Based on .
///
public abstract class VirtualizingPanelBase : VirtualizingPanel, IScrollInfo
{
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty MouseWheelDeltaItemProperty = DependencyProperty.Register(
nameof(MouseWheelDeltaItem),
typeof(int),
typeof(VirtualizingPanelBase),
new FrameworkPropertyMetadata(3));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty MouseWheelDeltaProperty = DependencyProperty.Register(
nameof(MouseWheelDelta),
typeof(double),
typeof(VirtualizingPanelBase),
new FrameworkPropertyMetadata(48.0));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ScrollLineDeltaItemProperty = DependencyProperty.Register(
nameof(ScrollLineDeltaItem),
typeof(int),
typeof(VirtualizingPanelBase),
new FrameworkPropertyMetadata(1));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ScrollLineDeltaProperty = DependencyProperty.Register(
nameof(ScrollLineDelta),
typeof(double),
typeof(VirtualizingPanelBase),
new FrameworkPropertyMetadata(16.0));
///
/// Items generator.
///
private IRecyclingItemContainerGenerator _itemContainerGenerator;
///
/// Owner of the displayed items.
///
private DependencyObject _itemsOwner;
///
/// Previously set visibility of the horizontal scroll bar.
///
private Visibility _previousHorizontalScrollBarVisibility = Visibility.Collapsed;
///
/// Previously set visibility of the vertical scroll bar.
///
private Visibility _previousVerticalScrollBarVisibility = Visibility.Collapsed;
///
/// Calculates the extent that would be needed to show all items.
///
protected abstract Size CalculateExtent(Size availableSize);
///
/// Gets the position of children from the generator.
///
protected virtual GeneratorPosition GetGeneratorPositionFromChildIndex(int childIndex)
{ return new GeneratorPosition(childIndex, 0); }
///
/// Gets item index from the generator.
///
protected int GetItemIndexFromChildIndex(int childIndex)
{
var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex);
return ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition);
}
///
/// Gets line down scroll amount.
///
protected abstract double GetLineDownScrollAmount();
///
/// Gets line left scroll amount.
///
protected abstract double GetLineLeftScrollAmount();
///
/// Gets line right scroll amount.
///
protected abstract double GetLineRightScrollAmount();
///
/// Gets line up scroll amount.
///
protected abstract double GetLineUpScrollAmount();
///
/// Gets mouse wheel down scroll amount.
///
protected abstract double GetMouseWheelDownScrollAmount();
///
/// Gets mouse wheel left scroll amount.
///
protected abstract double GetMouseWheelLeftScrollAmount();
///
/// Gets mouse wheel right scroll amount.
///
protected abstract double GetMouseWheelRightScrollAmount();
///
/// Gets mouse wheel up scroll amount.
///
protected abstract double GetMouseWheelUpScrollAmount();
///
/// Gets page down scroll amount.
///
protected abstract double GetPageDownScrollAmount();
///
/// Gets page left scroll amount.
///
protected abstract double GetPageLeftScrollAmount();
///
/// Gets page right scroll amount.
///
protected abstract double GetPageRightScrollAmount();
///
/// Gets page up scroll amount.
///
protected abstract double GetPageUpScrollAmount();
///
protected override Size MeasureOverride(Size availableSize)
{
/* Sometimes when scrolling the scrollbar gets hidden without any reason. In this case the "IsMeasureValid"
* property of the ScrollOwner is false. To prevent a infinite circle the mesasure call is ignored. */
if (ScrollOwner != null)
{
var verticalScrollBarGotHidden =
ScrollOwner.VerticalScrollBarVisibility == ScrollBarVisibility.Auto &&
ScrollOwner.ComputedVerticalScrollBarVisibility != Visibility.Visible &&
ScrollOwner.ComputedVerticalScrollBarVisibility != _previousVerticalScrollBarVisibility;
var horizontalScrollBarGotHidden =
ScrollOwner.HorizontalScrollBarVisibility == ScrollBarVisibility.Auto &&
ScrollOwner.ComputedHorizontalScrollBarVisibility != Visibility.Visible &&
ScrollOwner.ComputedHorizontalScrollBarVisibility != _previousHorizontalScrollBarVisibility;
_previousVerticalScrollBarVisibility = ScrollOwner.ComputedVerticalScrollBarVisibility;
_previousHorizontalScrollBarVisibility = ScrollOwner.ComputedHorizontalScrollBarVisibility;
if (!ScrollOwner.IsMeasureValid && verticalScrollBarGotHidden || horizontalScrollBarGotHidden)
{
return availableSize;
}
}
Size extent;
Size desiredSize;
if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
{
/* If the ItemsOwner is a group item the availableSize is ifinity.
* Therfore the vieport size provided by the group item is used. */
var viewportSize = groupItem.Constraints.Viewport.Size;
var headerSize = groupItem.HeaderDesiredSizes.PixelSize;
var availableWidth = Math.Max(viewportSize.Width - 5, 0); // left margin of 5 dp
var availableHeight = Math.Max(viewportSize.Height - headerSize.Height, 0);
availableSize = new Size(availableWidth, availableHeight);
extent = CalculateExtent(availableSize);
desiredSize = new Size(extent.Width, extent.Height);
Extent = extent;
Offset = groupItem.Constraints.Viewport.Location;
Viewport = groupItem.Constraints.Viewport.Size;
CacheLength = groupItem.Constraints.CacheLength;
CacheLengthUnit = groupItem.Constraints.CacheLengthUnit; // can be Item or Pixel
}
else
{
extent = CalculateExtent(availableSize);
var desiredWidth = Math.Min(availableSize.Width, extent.Width);
var desiredHeight = Math.Min(availableSize.Height, extent.Height);
desiredSize = new Size(desiredWidth, desiredHeight);
UpdateScrollInfo(desiredSize, extent);
CacheLength = GetCacheLength(ItemsOwner);
CacheLengthUnit = GetCacheLengthUnit(ItemsOwner); // can be Page, Item or Pixel
}
ItemRange = UpdateItemRange();
RealizeItems();
VirtualizeItems();
return desiredSize;
}
///
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
break;
}
}
///
/// Realizes visible and cached items.
///
protected virtual void RealizeItems()
{
var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex);
var childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1;
using var at = ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true);
for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++)
{
var child = (UIElement)ItemContainerGenerator.GenerateNext(out var isNewlyRealized);
if (isNewlyRealized || !InternalChildren.Contains(child)/*recycled*/
)
{
if (childIndex >= InternalChildren.Count)
{
AddInternalChild(child);
}
else
{
InsertInternalChild(childIndex, child);
}
ItemContainerGenerator.PrepareItemContainer(child);
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
if (child is not IHierarchicalVirtualizationAndScrollInfo groupItem)
{
continue;
}
groupItem.Constraints = new HierarchicalVirtualizationConstraints(
new VirtualizationCacheLength(0),
VirtualizationCacheLengthUnit.Item,
new Rect(0, 0, ViewportWidth, ViewportHeight));
child.Measure(new Size(ViewportWidth, ViewportHeight));
}
}
///
/// Sets horizontal scroll offset by given amount.
///
/// The value by which the offset is to be increased.
protected void ScrollHorizontal(double amount) { SetHorizontalOffset(HorizontalOffset + amount); }
///
/// Sets vertical scroll offset by given amount.
///
/// The value by which the offset is to be increased.
protected void ScrollVertical(double amount) { SetVerticalOffset(VerticalOffset + amount); }
///
/// Calculates the item range that is visible in the viewport or cached.
///
protected abstract ItemRange UpdateItemRange();
///
/// Updates scroll offset, extent and viewport.
///
protected virtual void UpdateScrollInfo(Size availableSize, Size extent)
{
var invalidateScrollInfo = false;
if (extent != Extent)
{
Extent = extent;
invalidateScrollInfo = true;
}
if (availableSize != Viewport)
{
Viewport = availableSize;
invalidateScrollInfo = true;
}
if (ViewportHeight != 0 && VerticalOffset != 0 && VerticalOffset + ViewportHeight + 1 >= ExtentHeight)
{
Offset = new Point(Offset.X, extent.Height - availableSize.Height);
invalidateScrollInfo = true;
}
if (ViewportWidth != 0 && HorizontalOffset != 0 && HorizontalOffset + ViewportWidth + 1 >= ExtentWidth)
{
Offset = new Point(extent.Width - availableSize.Width, Offset.Y);
invalidateScrollInfo = true;
}
if (invalidateScrollInfo)
{
ScrollOwner?.InvalidateScrollInfo();
}
}
///
/// Virtualizes (cleanups) no longer visible or cached items.
///
protected virtual void VirtualizeItems()
{
for (var childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--)
{
var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex);
var itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition);
if (itemIndex == -1 || ItemRange.Contains(itemIndex))
{
continue;
}
if (VirtualizationMode == VirtualizationMode.Recycling)
{
ItemContainerGenerator.Recycle(generatorPosition, 1);
}
else
{
ItemContainerGenerator.Remove(generatorPosition, 1);
}
RemoveInternalChildRange(childIndex, 1);
}
}
///
/// Gets the cache length before and after the viewport.
///
protected VirtualizationCacheLength CacheLength { get; private set; }
///
/// Gets the Unit of the cache length. Can be Pixel, Item or Page. When the ItemsOwner is a group item it can only
/// be pixel or item.
///
protected VirtualizationCacheLengthUnit CacheLengthUnit { get; private set; }
///
protected override bool CanHierarchicallyScrollAndVirtualizeCore => true;
///
/// Gets the .
///
protected Size Extent { get; private set; } = new Size(0, 0);
///
/// Gets a value indicating whether the panel is in VirtualizationMode.Recycling.
///
protected bool IsRecycling => VirtualizationMode == VirtualizationMode.Recycling;
///
/// Gets a value indicating whether the virtualizing is enabled.
///
protected bool IsVirtualizing => GetIsVirtualizing(ItemsControl);
///
/// Gets items container.
///
protected new IRecyclingItemContainerGenerator ItemContainerGenerator
{
get
{
if (_itemContainerGenerator is not null)
{
return _itemContainerGenerator;
}
/* Because of a bug in the framework the ItemContainerGenerator
* is null until InternalChildren accessed at least one time. */
_ = InternalChildren;
_itemContainerGenerator = (IRecyclingItemContainerGenerator)base.ItemContainerGenerator;
return _itemContainerGenerator;
}
}
///
/// Gets or sets the range of items that a realized in or cache.
///
protected ItemRange ItemRange { get; set; }
///
/// Gets items collection.
///
protected ReadOnlyCollection