/* 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 Items => ((ItemContainerGenerator)ItemContainerGenerator).Items; /// /// Gets the ItemsControl (e.g. ListView). /// protected ItemsControl ItemsControl => ItemsControl.GetItemsOwner(this); /// /// Gets the ItemsControl (e.g. ListView) or if the ItemsControl is grouping a GroupItem. /// protected DependencyObject ItemsOwner { get { if (_itemsOwner is not null) { return _itemsOwner; } /* Use reflection to access internal method because the public * GetItemsOwner method does always return the itmes control instead * of the real items owner for example the group item when grouping */ var getItemsOwnerInternalMethod = typeof(ItemsControl).GetMethod( "GetItemsOwnerInternal", BindingFlags.Static | BindingFlags.NonPublic, null, [typeof(DependencyObject)], null)!; _itemsOwner = (DependencyObject)getItemsOwnerInternalMethod.Invoke(null, [this])!; return _itemsOwner; } } /// /// Gets or sets the direction in which the panel scrolls when user turns the mouse wheel. /// protected ScrollDirection MouseWheelScrollDirection { get; set; } = ScrollDirection.Vertical; /// /// Gets the offset. /// protected Point Offset { get; private set; } = new(0, 0); /// /// Gets the scroll unit. /// protected ScrollUnit ScrollUnit => GetScrollUnit(ItemsControl); /// /// Gets the viewport. /// protected Size Viewport { get; private set; } = new Size(0, 0); /// /// Gets the virtualization mode. /// protected VirtualizationMode VirtualizationMode => GetVirtualizationMode(ItemsControl); /// public void LineDown() => ScrollVertical( ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineDownScrollAmount()); /// public void LineLeft() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineLeftScrollAmount()); /// public void LineRight() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineRightScrollAmount()); /// public void LineUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineUpScrollAmount()); /// public virtual Rect MakeVisible(Visual visual, Rect rectangle) { var pos = visual.TransformToAncestor(this).Transform(Offset); var scrollAmountX = 0d; var scrollAmountY = 0d; if (pos.X < Offset.X) { scrollAmountX = -(Offset.X - pos.X); } else if (pos.X + rectangle.Width > Offset.X + Viewport.Width) { var notVisibleX = pos.X + rectangle.Width - (Offset.X + Viewport.Width); var maxScrollX = pos.X - Offset.X; // keep left of the visual visible scrollAmountX = Math.Min(notVisibleX, maxScrollX); } if (pos.Y < Offset.Y) { scrollAmountY = -(Offset.Y - pos.Y); } else if (pos.Y + rectangle.Height > Offset.Y + Viewport.Height) { var notVisibleY = pos.Y + rectangle.Height - (Offset.Y + Viewport.Height); var maxScrollY = pos.Y - Offset.Y; // keep top of the visual visible scrollAmountY = Math.Min(notVisibleY, maxScrollY); } SetHorizontalOffset(Offset.X + scrollAmountX); SetVerticalOffset(Offset.Y + scrollAmountY); var visibleRectWidth = Math.Min(rectangle.Width, Viewport.Width); var visibleRectHeight = Math.Min(rectangle.Height, Viewport.Height); return new Rect(scrollAmountX, scrollAmountY, visibleRectWidth, visibleRectHeight); } /// public void MouseWheelDown() { if (MouseWheelScrollDirection == ScrollDirection.Vertical) { ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelDownScrollAmount()); } else { MouseWheelRight(); } } /// public void MouseWheelLeft() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelLeftScrollAmount()); /// public void MouseWheelRight() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelRightScrollAmount()); /// public void MouseWheelUp() { if (MouseWheelScrollDirection == ScrollDirection.Vertical) { ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelUpScrollAmount()); } else { MouseWheelLeft(); } } /// public void PageDown() => ScrollVertical( ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageDownScrollAmount()); /// public void PageLeft() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageLeftScrollAmount()); /// public void PageRight() => ScrollHorizontal( ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageRightScrollAmount()); /// public void PageUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageUpScrollAmount()); /// /// Sets the horizontal offset. /// public void SetHorizontalOffset(double offset) { if (offset < 0 || Viewport.Width >= Extent.Width) { offset = 0; } else if (offset + Viewport.Width >= Extent.Width) { offset = Extent.Width - Viewport.Width; } Offset = new Point(offset, Offset.Y); ScrollOwner?.InvalidateScrollInfo(); InvalidateMeasure(); } /// /// Sets the vertical offset. /// public void SetVerticalOffset(double offset) { if (offset < 0 || Viewport.Height >= Extent.Height) { offset = 0; } else if (offset + Viewport.Height >= Extent.Height) { offset = Extent.Height - Viewport.Height; } Offset = new Point(Offset.X, offset); ScrollOwner?.InvalidateScrollInfo(); InvalidateMeasure(); } /// /// Gets or sets a value indicating whether the content can be horizontally scrolled. /// public bool CanHorizontallyScroll { get; set; } /// /// Gets or sets a value indicating whether the content can be vertically scrolled. /// public bool CanVerticallyScroll { get; set; } /// /// Gets height of the . /// public double ExtentHeight => Extent.Height; /// /// Gets width of the . /// public double ExtentWidth => Extent.Width; /// /// Gets the horizontal offset. /// public double HorizontalOffset => Offset.X; /// /// Gets or sets the mouse wheel delta for pixel based scrolling. The default value is 48 dp. /// public double MouseWheelDelta { get => (double)GetValue(MouseWheelDeltaProperty); set => SetValue(MouseWheelDeltaProperty, value); } /// /// Gets or sets the mouse wheel delta for item based scrolling. The default value is 3 items. /// public int MouseWheelDeltaItem { get => (int)GetValue(MouseWheelDeltaItemProperty); set => SetValue(MouseWheelDeltaItemProperty, value); } /// /// Gets or sets the scroll line delta for pixel based scrolling. The default value is 16 dp. /// public double ScrollLineDelta { get => (double)GetValue(ScrollLineDeltaProperty); set => SetValue(ScrollLineDeltaProperty, value); } /// /// Gets or sets the scroll line delta for item based scrolling. The default value is 1 item. /// public int ScrollLineDeltaItem { get => (int)GetValue(ScrollLineDeltaItemProperty); set => SetValue(ScrollLineDeltaItemProperty, value); } /// /// Gets or sets the scroll owner. /// public ScrollViewer ScrollOwner { get; set; } /// /// Gets the vertical offset. /// public double VerticalOffset => Offset.Y; /// /// Gets the height. /// public double ViewportHeight => Viewport.Height; /// /// Gets the width. /// public double ViewportWidth => Viewport.Width; }