using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Controls.Primitives; using System.Windows.Controls; using System.Windows.Media; using System.Windows; namespace GeekDesk.Util.WrpaPanel { public abstract class VirtualizingPanelBase : VirtualizingPanel, IScrollInfo { public static readonly DependencyProperty ScrollLineDeltaProperty = DependencyProperty.Register(nameof(ScrollLineDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(16.0)); public static readonly DependencyProperty MouseWheelDeltaProperty = DependencyProperty.Register(nameof(MouseWheelDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(48.0)); public static readonly DependencyProperty ScrollLineDeltaItemProperty = DependencyProperty.Register(nameof(ScrollLineDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(1)); public static readonly DependencyProperty MouseWheelDeltaItemProperty = DependencyProperty.Register(nameof(MouseWheelDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(3)); public ScrollViewer? ScrollOwner { get; set; } public bool CanVerticallyScroll { get; set; } public bool CanHorizontallyScroll { get; set; } protected override bool CanHierarchicallyScrollAndVirtualizeCore => true; /// /// Scroll line delta for pixel based scrolling. The default value is 16 dp. /// public double ScrollLineDelta { get => (double)GetValue(ScrollLineDeltaProperty); set => SetValue(ScrollLineDeltaProperty, value); } /// /// Mouse wheel delta for pixel based scrolling. The default value is 48 dp. /// public double MouseWheelDelta { get => (double)GetValue(MouseWheelDeltaProperty); set => SetValue(MouseWheelDeltaProperty, value); } /// /// Scroll line delta for item based scrolling. The default value is 1 item. /// public double ScrollLineDeltaItem { get => (int)GetValue(ScrollLineDeltaItemProperty); set => SetValue(ScrollLineDeltaItemProperty, value); } /// /// Mouse wheel delta for item based scrolling. The default value is 3 items. /// public int MouseWheelDeltaItem { get => (int)GetValue(MouseWheelDeltaItemProperty); set => SetValue(MouseWheelDeltaItemProperty, value); } protected ScrollUnit ScrollUnit => GetScrollUnit(ItemsControl); /// /// The direction in which the panel scrolls when user turns the mouse wheel. /// protected ScrollDirection MouseWheelScrollDirection { get; set; } = ScrollDirection.Vertical; protected bool IsVirtualizing => GetIsVirtualizing(ItemsControl); protected VirtualizationMode VirtualizationMode => GetVirtualizationMode(ItemsControl); /// /// Returns true if the panel is in VirtualizationMode.Recycling, otherwise false. /// protected bool IsRecycling => VirtualizationMode == VirtualizationMode.Recycling; /// /// The cache length before and after the viewport. /// protected VirtualizationCacheLength CacheLength { get; private set; } /// /// 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; } /// /// The ItemsControl (e.g. ListView). /// protected ItemsControl ItemsControl => ItemsControl.GetItemsOwner(this); /// /// The ItemsControl (e.g. ListView) or if the ItemsControl is grouping a GroupItem. /// protected DependencyObject ItemsOwner { get { if (_itemsOwner is null) { /* 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 */ MethodInfo getItemsOwnerInternalMethod = typeof(ItemsControl).GetMethod( "GetItemsOwnerInternal", BindingFlags.Static | BindingFlags.NonPublic, null, new Type[] { typeof(DependencyObject) }, null )!; _itemsOwner = (DependencyObject)getItemsOwnerInternalMethod.Invoke(null, new object[] { this })!; } return _itemsOwner; } } private DependencyObject? _itemsOwner; protected ReadOnlyCollection Items => ((ItemContainerGenerator)ItemContainerGenerator).Items; protected new IRecyclingItemContainerGenerator ItemContainerGenerator { get { if (_itemContainerGenerator is null) { /* Because of a bug in the framework the ItemContainerGenerator * is null until InternalChildren accessed at least one time. */ var children = InternalChildren; _itemContainerGenerator = (IRecyclingItemContainerGenerator)base.ItemContainerGenerator; } return _itemContainerGenerator; } } private IRecyclingItemContainerGenerator? _itemContainerGenerator; public double ExtentWidth => Extent.Width; public double ExtentHeight => Extent.Height; protected Size Extent { get; private set; } = new Size(0, 0); public double HorizontalOffset => Offset.X; public double VerticalOffset => Offset.Y; protected Size Viewport { get; private set; } = new Size(0, 0); public double ViewportWidth => Viewport.Width; public double ViewportHeight => Viewport.Height; protected Point Offset { get; private set; } = new Point(0, 0); /// /// The range of items that a realized in viewport or cache. /// protected ItemRange ItemRange { get; set; } private Visibility previousVerticalScrollBarVisibility = Visibility.Collapsed; private Visibility previousHorizontalScrollBarVisibility = Visibility.Collapsed; protected virtual void UpdateScrollInfo(Size availableSize, Size extent) { bool 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(); } } public virtual Rect MakeVisible(Visual visual, Rect rectangle) { Point pos = visual.TransformToAncestor(this).Transform(Offset); double scrollAmountX = 0; double scrollAmountY = 0; if (pos.X < Offset.X) { scrollAmountX = -(Offset.X - pos.X); } else if ((pos.X + rectangle.Width) > (Offset.X + Viewport.Width)) { double notVisibleX = (pos.X + rectangle.Width) - (Offset.X + Viewport.Width); double 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)) { double notVisibleY = (pos.Y + rectangle.Height) - (Offset.Y + Viewport.Height); double maxScrollY = pos.Y - Offset.Y; // keep top of the visual visible scrollAmountY = Math.Min(notVisibleY, maxScrollY); } SetHorizontalOffset(Offset.X + scrollAmountX); SetVerticalOffset(Offset.Y + scrollAmountY); double visibleRectWidth = Math.Min(rectangle.Width, Viewport.Width); double visibleRectHeight = Math.Min(rectangle.Height, Viewport.Height); return new Rect(scrollAmountX, scrollAmountY, visibleRectWidth, visibleRectHeight); } 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; } } protected int GetItemIndexFromChildIndex(int childIndex) { var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); return ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); } protected virtual GeneratorPosition GetGeneratorPositionFromChildIndex(int childIndex) { return new GeneratorPosition(childIndex, 0); } 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) { bool verticalScrollBarGotHidden = ScrollOwner.VerticalScrollBarVisibility == ScrollBarVisibility.Auto && ScrollOwner.ComputedVerticalScrollBarVisibility != Visibility.Visible && ScrollOwner.ComputedVerticalScrollBarVisibility != previousVerticalScrollBarVisibility; bool 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; } } var groupItem = ItemsOwner as IHierarchicalVirtualizationAndScrollInfo; Size extent; Size desiredSize; if (groupItem != null) { /* 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; double availableWidth = Math.Max(viewportSize.Width - 5, 0); // left margin of 5 dp double 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); double desiredWidth = Math.Min(availableSize.Width, extent.Width); double 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; } /// /// Realizes visible and cached items. /// protected virtual void RealizeItems() { var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex); int childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1; using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true)) { for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++) { UIElement child = (UIElement)ItemContainerGenerator.GenerateNext(out bool isNewlyRealized); if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child)) { if (childIndex >= InternalChildren.Count) { AddInternalChild(child); } else { InsertInternalChild(childIndex, child); } ItemContainerGenerator.PrepareItemContainer(child); child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); } if (child is IHierarchicalVirtualizationAndScrollInfo groupItem) { groupItem.Constraints = new HierarchicalVirtualizationConstraints( new VirtualizationCacheLength(0), VirtualizationCacheLengthUnit.Item, new Rect(0, 0, ViewportWidth, ViewportHeight)); child.Measure(new Size(ViewportWidth, ViewportHeight)); } } } } /// /// Virtualizes (cleanups) no longer visible or cached items. /// protected virtual void VirtualizeItems() { for (int childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--) { var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); if (itemIndex != -1 && !ItemRange.Contains(itemIndex)) { if (VirtualizationMode == VirtualizationMode.Recycling) { ItemContainerGenerator.Recycle(generatorPosition, 1); } else { ItemContainerGenerator.Remove(generatorPosition, 1); } RemoveInternalChildRange(childIndex, 1); } } } /// /// Calculates the extent that would be needed to show all items. /// protected abstract Size CalculateExtent(Size availableSize); /// /// Calculates the item range that is visible in the viewport or cached. /// protected abstract ItemRange UpdateItemRange(); 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(); } 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(); } protected void ScrollVertical(double amount) { SetVerticalOffset(VerticalOffset + amount); } protected void ScrollHorizontal(double amount) { SetHorizontalOffset(HorizontalOffset + amount); } public void LineUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineUpScrollAmount()); 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 MouseWheelUp() { if (MouseWheelScrollDirection == ScrollDirection.Vertical) { ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelUpScrollAmount()); } else { MouseWheelLeft(); } } 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 PageUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageUpScrollAmount()); 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()); protected abstract double GetLineUpScrollAmount(); protected abstract double GetLineDownScrollAmount(); protected abstract double GetLineLeftScrollAmount(); protected abstract double GetLineRightScrollAmount(); protected abstract double GetMouseWheelUpScrollAmount(); protected abstract double GetMouseWheelDownScrollAmount(); protected abstract double GetMouseWheelLeftScrollAmount(); protected abstract double GetMouseWheelRightScrollAmount(); protected abstract double GetPageUpScrollAmount(); protected abstract double GetPageDownScrollAmount(); protected abstract double GetPageLeftScrollAmount(); protected abstract double GetPageRightScrollAmount(); } }