diff options
Diffstat (limited to 'MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs')
| -rw-r--r-- | MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs | 735 |
1 files changed, 735 insertions, 0 deletions
diff --git a/MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs b/MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs new file mode 100644 index 000000000..e9753ccea --- /dev/null +++ b/MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs @@ -0,0 +1,735 @@ +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 +{ + /// <summary> + /// 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. + /// </summary> + [DefaultProperty("Orientation")] + public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo + { + /// <summary> + /// Identifies the ItemHeight dependency property. + /// </summary> + public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(100.0, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); + /// <summary> + /// Identifies the Orientation dependency property. + /// </summary> + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(VirtualizingWrapPanel), new PropertyMetadata(Orientation.Horizontal, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); + /// <summary> + /// Identifies the ItemWidth dependency property. + /// </summary> + public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(100.0, new PropertyChangedCallback(VirtualizingWrapPanel.OnAppearancePropertyChanged))); + /// <summary> + /// Identifies the ScrollStep dependency property. + /// </summary> + 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; + /// <summary> + /// Gets or sets a value that specifies the height of all items that are + /// contained within a VirtualizingWrapPanel. This is a dependency property. + /// </summary> + public double ItemHeight + { + get + { + return (double)base.GetValue(VirtualizingWrapPanel.ItemHeightProperty); + } + set + { + base.SetValue(VirtualizingWrapPanel.ItemHeightProperty, value); + } + } + /// <summary> + /// Gets or sets a value that specifies the width of all items that are + /// contained within a VirtualizingWrapPanel. This is a dependency property. + /// </summary> + public double ItemWidth + { + get + { + return (double)base.GetValue(VirtualizingWrapPanel.ItemWidthProperty); + } + set + { + base.SetValue(VirtualizingWrapPanel.ItemWidthProperty, value); + } + } + /// <summary> + /// Gets or sets a value that specifies the dimension in which child + /// content is arranged. This is a dependency property. + /// </summary> + public Orientation Orientation + { + get + { + return (Orientation)base.GetValue(VirtualizingWrapPanel.OrientationProperty); + } + set + { + base.SetValue(VirtualizingWrapPanel.OrientationProperty, value); + } + } + /// <summary> + /// Gets or sets a value that indicates whether scrolling on the horizontal axis is possible. + /// </summary> + public bool CanHorizontallyScroll + { + get + { + return this.canHorizontallyScroll; + } + set + { + if (this.canHorizontallyScroll != value) + { + this.canHorizontallyScroll = value; + base.InvalidateMeasure(); + } + } + } + /// <summary> + /// Gets or sets a value that indicates whether scrolling on the vertical axis is possible. + /// </summary> + public bool CanVerticallyScroll + { + get + { + return this.canVerticallyScroll; + } + set + { + if (this.canVerticallyScroll != value) + { + this.canVerticallyScroll = value; + base.InvalidateMeasure(); + } + } + } + /// <summary> + /// Gets or sets a ScrollViewer element that controls scrolling behavior. + /// </summary> + public ScrollViewer ScrollOwner + { + get + { + return this.scrollOwner; + } + set + { + this.scrollOwner = value; + } + } + /// <summary> + /// Gets the vertical offset of the scrolled content. + /// </summary> + public double VerticalOffset + { + get + { + return this.contentOffset.Y; + } + } + /// <summary> + /// Gets the vertical size of the viewport for this content. + /// </summary> + public double ViewportHeight + { + get + { + return this.viewport.Height; + } + } + /// <summary> + /// Gets the horizontal size of the viewport for this content. + /// </summary> + public double ViewportWidth + { + get + { + return this.viewport.Width; + } + } + /// <summary> + /// Gets or sets a value for mouse wheel scroll step. + /// </summary> + public double ScrollStep + { + get + { + return (double)base.GetValue(VirtualizingWrapPanel.ScrollStepProperty); + } + set + { + base.SetValue(VirtualizingWrapPanel.ScrollStepProperty, value); + } + } + /// <summary> + /// Gets the vertical size of the extent. + /// </summary> + public double ExtentHeight + { + get + { + return this.contentExtent.Height; + } + } + /// <summary> + /// Gets the horizontal size of the extent. + /// </summary> + public double ExtentWidth + { + get + { + return this.contentExtent.Width; + } + } + /// <summary> + /// Gets the horizontal offset of the scrolled content. + /// </summary> + public double HorizontalOffset + { + get + { + return this.contentOffset.X; + } + } + /// <summary> + /// Scrolls down within content by one logical unit. + /// </summary> + public void LineDown() + { + this.SetVerticalOffset(this.VerticalOffset + this.ScrollStep); + } + /// <summary> + /// Scrolls left within content by one logical unit. + /// </summary> + public void LineLeft() + { + this.SetHorizontalOffset(this.HorizontalOffset - this.ScrollStep); + } + /// <summary> + /// Scrolls right within content by one logical unit. + /// </summary> + public void LineRight() + { + this.SetHorizontalOffset(this.HorizontalOffset + this.ScrollStep); + } + /// <summary> + /// Scrolls up within content by one logical unit. + /// </summary> + public void LineUp() + { + this.SetVerticalOffset(this.VerticalOffset - this.ScrollStep); + } + /// <summary> + /// Forces content to scroll until the coordinate space of a Visual object is visible. + /// </summary> + public Rect MakeVisible(Visual visual, Rect rectangle) + { + this.MakeVisible(visual as UIElement); + return rectangle; + } + /// <summary> + /// Scrolls down within content after a user clicks the wheel button on a mouse. + /// </summary> + public void MouseWheelDown() + { + this.SetVerticalOffset(this.VerticalOffset + this.ScrollStep); + } + /// <summary> + /// Scrolls left within content after a user clicks the wheel button on a mouse. + /// </summary> + public void MouseWheelLeft() + { + this.SetHorizontalOffset(this.HorizontalOffset - this.ScrollStep); + } + /// <summary> + /// Scrolls right within content after a user clicks the wheel button on a mouse. + /// </summary> + public void MouseWheelRight() + { + this.SetHorizontalOffset(this.HorizontalOffset + this.ScrollStep); + } + /// <summary> + /// Scrolls up within content after a user clicks the wheel button on a mouse. + /// </summary> + public void MouseWheelUp() + { + this.SetVerticalOffset(this.VerticalOffset - this.ScrollStep); + } + /// <summary> + /// Scrolls down within content by one page. + /// </summary> + public void PageDown() + { + this.SetVerticalOffset(this.VerticalOffset + this.ViewportHeight); + } + /// <summary> + /// Scrolls left within content by one page. + /// </summary> + public void PageLeft() + { + this.SetHorizontalOffset(this.HorizontalOffset - this.ViewportHeight); + } + /// <summary> + /// Scrolls right within content by one page. + /// </summary> + public void PageRight() + { + this.SetHorizontalOffset(this.HorizontalOffset + this.ViewportHeight); + } + /// <summary> + /// Scrolls up within content by one page. + /// </summary> + public void PageUp() + { + this.SetVerticalOffset(this.VerticalOffset - this.viewport.Height); + } + /// <summary> + /// Sets the amount of vertical offset. + /// </summary> + 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(); + } + /// <summary> + /// Sets the amount of horizontal offset. + /// </summary> + 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(); + } + /// <summary> + /// Note: Works only for vertical. + /// </summary> + internal void PageLast() + { + this.contentOffset.Y = this.ExtentHeight; + if (this.ScrollOwner != null) + { + this.ScrollOwner.InvalidateScrollInfo(); + } + base.InvalidateMeasure(); + } + /// <summary> + /// Note: Works only for vertical. + /// </summary> + internal void PageFirst() + { + this.contentOffset.Y = 0.0; + if (this.ScrollOwner != null) + { + this.ScrollOwner.InvalidateScrollInfo(); + } + base.InvalidateMeasure(); + } + /// <summary> + /// When items are removed, remove the corresponding UI if necessary. + /// </summary> + /// <param name="sender"></param> + /// <param name="args"></param> + 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; + } + } + /// <summary> + /// Measure the children. + /// </summary> + /// <param name="availableSize">The available size.</param> + /// <returns>The desired size.</returns> + 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); + } + + /// <summary> + /// Arranges the children. + /// </summary> + /// <param name="finalSize">The available size.</param> + /// <returns>The used size.</returns> + 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<UIElement>(); + } + ScrollViewer scrollViewer = element.ParentOfType<ScrollViewer>(); + 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; + } + } +}
\ No newline at end of file |
