using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace MediaBrowser.UI.Controls { /// /// http://www.codeproject.com/Articles/75847/Virtualizing-WrapPanel /// Positions child elements in sequential position from left to right, breaking content /// to the next line at the edge of the containing box. Subsequent ordering happens /// sequentially from top to bottom or from right to left, depending on the value of /// the Orientation property. /// [DefaultProperty("Orientation")] public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo { /// /// Identifies the ItemHeight dependency property. /// public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(100.0, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); /// /// Identifies the Orientation dependency property. /// public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(VirtualizingWrapPanel), new PropertyMetadata(Orientation.Horizontal, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); /// /// Identifies the ItemWidth dependency property. /// public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(100.0, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); /// /// Identifies the ScrollStep dependency property. /// public static readonly DependencyProperty ScrollStepProperty = DependencyProperty.Register("ScrollStep", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(10.0, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); private bool canHorizontallyScroll; private bool canVerticallyScroll; private Size contentExtent = new Size(0.0, 0.0); private Point contentOffset = default(Point); private ScrollViewer scrollOwner; private Size viewport = new Size(0.0, 0.0); private int previousItemCount; /// /// Gets or sets a value that specifies the height of all items that are /// contained within a VirtualizingWrapPanel. This is a dependency property. /// public double ItemHeight { get { return (double)base.GetValue(VirtualizingWrapPanel.ItemHeightProperty); } set { base.SetValue(VirtualizingWrapPanel.ItemHeightProperty, value); } } /// /// Gets or sets a value that specifies the width of all items that are /// contained within a VirtualizingWrapPanel. This is a dependency property. /// public double ItemWidth { get { return (double)base.GetValue(VirtualizingWrapPanel.ItemWidthProperty); } set { base.SetValue(VirtualizingWrapPanel.ItemWidthProperty, value); } } /// /// Gets or sets a value that specifies the dimension in which child /// content is arranged. This is a dependency property. /// public Orientation Orientation { get { return (Orientation)base.GetValue(VirtualizingWrapPanel.OrientationProperty); } set { base.SetValue(VirtualizingWrapPanel.OrientationProperty, value); } } /// /// Gets or sets a value that indicates whether scrolling on the horizontal axis is possible. /// public bool CanHorizontallyScroll { get { return this.canHorizontallyScroll; } set { if (this.canHorizontallyScroll != value) { this.canHorizontallyScroll = value; base.InvalidateMeasure(); } } } /// /// Gets or sets a value that indicates whether scrolling on the vertical axis is possible. /// public bool CanVerticallyScroll { get { return this.canVerticallyScroll; } set { if (this.canVerticallyScroll != value) { this.canVerticallyScroll = value; base.InvalidateMeasure(); } } } /// /// Gets or sets a ScrollViewer element that controls scrolling behavior. /// public ScrollViewer ScrollOwner { get { return this.scrollOwner; } set { this.scrollOwner = value; } } /// /// Gets the vertical offset of the scrolled content. /// public double VerticalOffset { get { return this.contentOffset.Y; } } /// /// Gets the vertical size of the viewport for this content. /// public double ViewportHeight { get { return this.viewport.Height; } } /// /// Gets the horizontal size of the viewport for this content. /// public double ViewportWidth { get { return this.viewport.Width; } } /// /// Gets or sets a value for mouse wheel scroll step. /// public double ScrollStep { get { return (double)base.GetValue(VirtualizingWrapPanel.ScrollStepProperty); } set { base.SetValue(VirtualizingWrapPanel.ScrollStepProperty, value); } } /// /// Gets the vertical size of the extent. /// public double ExtentHeight { get { return this.contentExtent.Height; } } /// /// Gets the horizontal size of the extent. /// public double ExtentWidth { get { return this.contentExtent.Width; } } /// /// Gets the horizontal offset of the scrolled content. /// public double HorizontalOffset { get { return this.contentOffset.X; } } /// /// Scrolls down within content by one logical unit. /// public void LineDown() { this.SetVerticalOffset(this.VerticalOffset + this.ScrollStep); } /// /// Scrolls left within content by one logical unit. /// public void LineLeft() { this.SetHorizontalOffset(this.HorizontalOffset - this.ScrollStep); } /// /// Scrolls right within content by one logical unit. /// public void LineRight() { this.SetHorizontalOffset(this.HorizontalOffset + this.ScrollStep); } /// /// Scrolls up within content by one logical unit. /// public void LineUp() { this.SetVerticalOffset(this.VerticalOffset - this.ScrollStep); } /// /// Forces content to scroll until the coordinate space of a Visual object is visible. /// public Rect MakeVisible(Visual visual, Rect rectangle) { this.MakeVisible(visual as UIElement); return rectangle; } /// /// Scrolls down within content after a user clicks the wheel button on a mouse. /// public void MouseWheelDown() { this.SetVerticalOffset(this.VerticalOffset + this.ScrollStep); } /// /// Scrolls left within content after a user clicks the wheel button on a mouse. /// public void MouseWheelLeft() { this.SetHorizontalOffset(this.HorizontalOffset - this.ScrollStep); } /// /// Scrolls right within content after a user clicks the wheel button on a mouse. /// public void MouseWheelRight() { this.SetHorizontalOffset(this.HorizontalOffset + this.ScrollStep); } /// /// Scrolls up within content after a user clicks the wheel button on a mouse. /// public void MouseWheelUp() { this.SetVerticalOffset(this.VerticalOffset - this.ScrollStep); } /// /// Scrolls down within content by one page. /// public void PageDown() { this.SetVerticalOffset(this.VerticalOffset + this.ViewportHeight); } /// /// Scrolls left within content by one page. /// public void PageLeft() { this.SetHorizontalOffset(this.HorizontalOffset - this.ViewportHeight); } /// /// Scrolls right within content by one page. /// public void PageRight() { this.SetHorizontalOffset(this.HorizontalOffset + this.ViewportHeight); } /// /// Scrolls up within content by one page. /// public void PageUp() { this.SetVerticalOffset(this.VerticalOffset - this.viewport.Height); } /// /// Sets the amount of vertical offset. /// public void SetVerticalOffset(double offset) { if (offset < 0.0 || this.ViewportHeight >= this.ExtentHeight) { offset = 0.0; } else { if (offset + this.ViewportHeight >= this.ExtentHeight) { offset = this.ExtentHeight - this.ViewportHeight; } } this.contentOffset.Y = offset; if (this.ScrollOwner != null) { this.ScrollOwner.InvalidateScrollInfo(); } base.InvalidateMeasure(); } /// /// Sets the amount of horizontal offset. /// public void SetHorizontalOffset(double offset) { if (offset < 0.0 || this.ViewportWidth >= this.ExtentWidth) { offset = 0.0; } else { if (offset + this.ViewportWidth >= this.ExtentWidth) { offset = this.ExtentWidth - this.ViewportWidth; } } this.contentOffset.X = offset; if (this.ScrollOwner != null) { this.ScrollOwner.InvalidateScrollInfo(); } base.InvalidateMeasure(); } /// /// Note: Works only for vertical. /// internal void PageLast() { this.contentOffset.Y = this.ExtentHeight; if (this.ScrollOwner != null) { this.ScrollOwner.InvalidateScrollInfo(); } base.InvalidateMeasure(); } /// /// Note: Works only for vertical. /// internal void PageFirst() { this.contentOffset.Y = 0.0; if (this.ScrollOwner != null) { this.ScrollOwner.InvalidateScrollInfo(); } base.InvalidateMeasure(); } /// /// When items are removed, remove the corresponding UI if necessary. /// /// /// protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: base.RemoveInternalChildRange(args.Position.Index, args.ItemUICount); return; case NotifyCollectionChangedAction.Reset: { ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); if (itemsControl != null) { if (this.previousItemCount != itemsControl.Items.Count) { if (this.Orientation == Orientation.Horizontal) { this.SetVerticalOffset(0.0); } else { this.SetHorizontalOffset(0.0); } } this.previousItemCount = itemsControl.Items.Count; } return; } default: return; } } /// /// Measure the children. /// /// The available size. /// The desired size. protected override Size MeasureOverride(Size availableSize) { this.InvalidateScrollInfo(availableSize); int firstVisibleIndex; int lastVisibleIndex; if (this.Orientation == Orientation.Horizontal) { this.GetVerticalVisibleRange(out firstVisibleIndex, out lastVisibleIndex); } else { this.GetHorizontalVisibleRange(out firstVisibleIndex, out lastVisibleIndex); } UIElementCollection children = base.Children; IItemContainerGenerator generator = base.ItemContainerGenerator; if (generator != null) { GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleIndex); int childIndex = (startPos.Offset == 0) ? startPos.Index : (startPos.Index + 1); if (childIndex == -1) { this.RefreshOffset(); } using (generator.StartAt(startPos, GeneratorDirection.Forward, true)) { int itemIndex = firstVisibleIndex; while (itemIndex <= lastVisibleIndex) { bool newlyRealized; UIElement child = generator.GenerateNext(out newlyRealized) as UIElement; if (newlyRealized) { if (childIndex >= children.Count) { base.AddInternalChild(child); } else { base.InsertInternalChild(childIndex, child); } generator.PrepareItemContainer(child); } if (child != null) { child.Measure(new Size(this.ItemWidth, this.ItemHeight)); } itemIndex++; childIndex++; } } this.CleanUpChildren(firstVisibleIndex, lastVisibleIndex); } if (IsCloseTo(availableSize.Height, double.PositiveInfinity) || IsCloseTo(availableSize.Width, double.PositiveInfinity)) { return base.MeasureOverride(availableSize); } var itemsControl = ItemsControl.GetItemsOwner(this); var numItems = itemsControl.Items.Count; var width = availableSize.Width; var height = availableSize.Height; if (Orientation == Orientation.Vertical) { var numRows = Math.Floor(availableSize.Height / ItemHeight); height = numRows * ItemHeight; var requiredColumns = Math.Ceiling(numItems / numRows); width = Math.Min(requiredColumns * ItemWidth, width); } else { var numColumns = Math.Floor(availableSize.Width / ItemWidth); width = numColumns * ItemWidth; //if (numItems > 0 && numItems < numColumns) //{ // width = Math.Min(numColumns, numItems) * ItemWidth; //} var requiredRows = Math.Ceiling(numItems / numColumns); height = Math.Min(requiredRows * ItemHeight, height); } return new Size(width, height); } /// /// Arranges the children. /// /// The available size. /// The used size. protected override Size ArrangeOverride(Size finalSize) { bool isHorizontal = this.Orientation == Orientation.Horizontal; this.InvalidateScrollInfo(finalSize); int i = 0; foreach (object item in base.Children) { this.ArrangeChild(isHorizontal, finalSize, i++, item as UIElement); } return finalSize; } private static void OnAppearancePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { UIElement panel = d as UIElement; if (panel != null) { panel.InvalidateMeasure(); } } private void MakeVisible(UIElement element) { ItemContainerGenerator generator = base.ItemContainerGenerator.GetItemContainerGeneratorForPanel(this); if (element != null && generator != null) { for (int itemIndex = generator.IndexFromContainer(element); itemIndex == -1; itemIndex = generator.IndexFromContainer(element)) { element = element.ParentOfType(); } ScrollViewer scrollViewer = element.ParentOfType(); if (scrollViewer != null) { GeneralTransform elementTransform = element.TransformToVisual(scrollViewer); Rect elementRectangle = elementTransform.TransformBounds(new Rect(new Point(0.0, 0.0), element.RenderSize)); if (this.Orientation == Orientation.Horizontal) { var padding = ItemHeight / 3; if (elementRectangle.Bottom > this.ViewportHeight) { this.SetVerticalOffset(this.contentOffset.Y + elementRectangle.Bottom - this.ViewportHeight + padding); return; } if (elementRectangle.Top < 0.0) { this.SetVerticalOffset(this.contentOffset.Y + elementRectangle.Top - padding); return; } } else { var padding = ItemWidth / 3; if (elementRectangle.Right > this.ViewportWidth) { this.SetHorizontalOffset(this.contentOffset.X + elementRectangle.Right - this.ViewportWidth + padding); return; } if (elementRectangle.Left < 0.0) { this.SetHorizontalOffset(this.contentOffset.X + elementRectangle.Left - padding); } } } } } private void GetVerticalVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex) { int childrenPerRow = this.GetVerticalChildrenCountPerRow(this.contentExtent); firstVisibleItemIndex = (int)Math.Floor(this.VerticalOffset / this.ItemHeight) * childrenPerRow; lastVisibleItemIndex = (int)Math.Ceiling((this.VerticalOffset + this.ViewportHeight) / this.ItemHeight) * childrenPerRow - 1; this.AdjustVisibleRange(ref firstVisibleItemIndex, ref lastVisibleItemIndex); } private void GetHorizontalVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex) { int childrenPerRow = this.GetHorizontalChildrenCountPerRow(this.contentExtent); firstVisibleItemIndex = (int)Math.Floor(this.HorizontalOffset / this.ItemWidth) * childrenPerRow; lastVisibleItemIndex = (int)Math.Ceiling((this.HorizontalOffset + this.ViewportWidth) / this.ItemWidth) * childrenPerRow - 1; this.AdjustVisibleRange(ref firstVisibleItemIndex, ref lastVisibleItemIndex); } private void AdjustVisibleRange(ref int firstVisibleItemIndex, ref int lastVisibleItemIndex) { firstVisibleItemIndex--; lastVisibleItemIndex++; ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); if (itemsControl != null) { if (firstVisibleItemIndex < 0) { firstVisibleItemIndex = 0; } if (lastVisibleItemIndex >= itemsControl.Items.Count) { lastVisibleItemIndex = itemsControl.Items.Count - 1; } } } private void CleanUpChildren(int minIndex, int maxIndex) { UIElementCollection children = base.Children; IItemContainerGenerator generator = base.ItemContainerGenerator; for (int i = children.Count - 1; i >= 0; i--) { GeneratorPosition pos = new GeneratorPosition(i, 0); int itemIndex = generator.IndexFromGeneratorPosition(pos); if (itemIndex < minIndex || itemIndex > maxIndex) { generator.Remove(pos, 1); base.RemoveInternalChildRange(i, 1); } } } private void ArrangeChild(bool isHorizontal, Size finalSize, int index, UIElement child) { if (child == null) { return; } int count = isHorizontal ? this.GetVerticalChildrenCountPerRow(finalSize) : this.GetHorizontalChildrenCountPerRow(finalSize); int itemIndex = base.ItemContainerGenerator.IndexFromGeneratorPosition(new GeneratorPosition(index, 0)); int row = isHorizontal ? (itemIndex / count) : (itemIndex % count); int column = isHorizontal ? (itemIndex % count) : (itemIndex / count); Rect rect = new Rect((double)column * this.ItemWidth, (double)row * this.ItemHeight, this.ItemWidth, this.ItemHeight); if (isHorizontal) { rect.Y -= this.VerticalOffset; } else { rect.X -= this.HorizontalOffset; } child.Arrange(rect); } private void InvalidateScrollInfo(Size availableSize) { ItemsControl ownerItemsControl = ItemsControl.GetItemsOwner(this); if (ownerItemsControl != null) { Size extent = this.GetExtent(availableSize, ownerItemsControl.Items.Count); if (extent != this.contentExtent) { this.contentExtent = extent; this.RefreshOffset(); } if (availableSize != this.viewport) { this.viewport = availableSize; this.InvalidateScrollOwner(); } } } private void RefreshOffset() { if (this.Orientation == Orientation.Horizontal) { this.SetVerticalOffset(this.VerticalOffset); return; } this.SetHorizontalOffset(this.HorizontalOffset); } private void InvalidateScrollOwner() { if (this.ScrollOwner != null) { this.ScrollOwner.InvalidateScrollInfo(); } } private Size GetExtent(Size availableSize, int itemCount) { if (this.Orientation == Orientation.Horizontal) { int childrenPerRow = this.GetVerticalChildrenCountPerRow(availableSize); return new Size((double)childrenPerRow * this.ItemWidth, this.ItemHeight * Math.Ceiling((double)itemCount / (double)childrenPerRow)); } int childrenPerRow2 = this.GetHorizontalChildrenCountPerRow(availableSize); return new Size(this.ItemWidth * Math.Ceiling((double)itemCount / (double)childrenPerRow2), (double)childrenPerRow2 * this.ItemHeight); } private int GetVerticalChildrenCountPerRow(Size availableSize) { int childrenCountPerRow; if (availableSize.Width == double.PositiveInfinity) { childrenCountPerRow = base.Children.Count; } else { childrenCountPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / this.ItemWidth)); } return childrenCountPerRow; } private int GetHorizontalChildrenCountPerRow(Size availableSize) { int childrenCountPerRow; if (availableSize.Height == double.PositiveInfinity) { childrenCountPerRow = base.Children.Count; } else { childrenCountPerRow = Math.Max(1, (int)Math.Floor(availableSize.Height / this.ItemHeight)); } return childrenCountPerRow; } private static bool IsCloseTo(double value1, double value2) { return AreClose(value1, value2); } private static bool AreClose(double value1, double value2) { if (value1 == value2) { return true; } double num = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * 2.2204460492503131E-16; double num2 = value1 - value2; return -num < num2 && num > num2; } } }