jellyfin-server/MediaBrowser.UI.Controls/VirtualizingWrapPanel.cs
2013-02-20 20:33:05 -05:00

735 lines
28 KiB
C#

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;
}
}
}