Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
VirtualizingTilePanel.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Windows;
5 using System.Windows.Controls;
6 using System.Windows.Controls.Primitives;
7 using System.Windows.Media;
8 using System.Windows.Threading;
9 
10 namespace SiliconStudio.Presentation.Controls
11 {
12  /// <summary>
13  /// This class describes a Panel similar to a <see cref="WrapPanel"/> except that items have a reserved space that is equal to the size of the largest items. Every item
14  /// is aligned vertically and horizontally such as if they were in a grid.
15  /// </summary>
17  {
18  private int itemsPerLine = -1;
19  private int lineCount = -1;
20  private int itemCount = -1;
21 
22  /// <summary>
23  /// Identifies the <see cref="Orientation"/> dependency property.
24  /// </summary>
25  public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
26  "Orientation",
27  typeof(Orientation),
28  typeof(VirtualizingTilePanel),
29  new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsMeasure));
30 
31  /// <summary>
32  /// Identifies the <see cref="MinimumItemSpacing"/> dependency property.
33  /// </summary>
34  public static readonly DependencyProperty MinimumItemSpacingProperty = DependencyProperty.Register(
35  "MinimumItemSpacing",
36  typeof(double),
37  typeof(VirtualizingTilePanel),
38  new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure),
39  ValidateMinMaxItemSpacing);
40 
41  /// <summary>
42  /// Identifies the <see cref="MaximumItemSpacing"/> dependency property.
43  /// </summary>
44  public static readonly DependencyProperty MaximumItemSpacingProperty = DependencyProperty.Register(
45  "MaximumItemSpacing",
46  typeof(double),
47  typeof(VirtualizingTilePanel),
48  new FrameworkPropertyMetadata(double.MaxValue, FrameworkPropertyMetadataOptions.AffectsMeasure),
49  ValidateMinMaxItemSpacing);
50 
51  /// <summary>
52  /// Identifies the <see cref="ItemSlotSize"/> dependency property.
53  /// </summary>
54  public static readonly DependencyProperty ItemSlotSizeProperty = DependencyProperty.Register(
55  "ItemSlotSize",
56  typeof(Size),
57  typeof(VirtualizingTilePanel),
58  new FrameworkPropertyMetadata(new Size(64.0, 64.0), FrameworkPropertyMetadataOptions.AffectsMeasure),
59  ValidateSize);
60 
61  /// <summary>
62  /// Gets or sets the orientation of the Tile Panel.
63  /// </summary>
64  public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } }
65 
66  /// <summary>
67  /// Gets or sets the minimum spacing allowed between items.
68  /// </summary>
69  public double MinimumItemSpacing { get { return (double)GetValue(MinimumItemSpacingProperty); } set { SetValue(MinimumItemSpacingProperty, value); } }
70 
71  /// <summary>
72  /// Gets or sets the maximum spacing allowed between items.
73  /// </summary>
74  public double MaximumItemSpacing { get { return (double)GetValue(MaximumItemSpacingProperty); } set { SetValue(MaximumItemSpacingProperty, value); } }
75 
76  /// <summary>
77  /// Gets or sets the item slot size to use in case all items requires infinite or empty size.
78  /// </summary>
79  public Size ItemSlotSize { get { return (Size)GetValue(ItemSlotSizeProperty); } set { SetValue(ItemSlotSizeProperty, value); } }
80 
81  public int ItemsPerLine { get { return itemsPerLine; } }
82 
83  public int ItemCount { get { return itemCount; } }
84 
85  private static bool ValidateMinMaxItemSpacing(object value)
86  {
87  if ((value is double) == false)
88  return false;
89 
90  var v = (double)value;
91 
92  return v >= 0.0 && double.IsInfinity(v) == false;
93  }
94 
95  private static bool ValidateSize(object value)
96  {
97  if ((value is Size) == false)
98  return false;
99 
100  var size = (Size)value;
101 
102  return size.Width >= 1.0 &&
103  size.Height >= 1.0 &&
104  double.IsInfinity(size.Width) == false &&
105  double.IsInfinity(size.Height) == false;
106  }
107 
108  public void GetVisibilityRange(Size panelSize, out int firstVisibleItemIndex, out int lastVisibleItemIndex)
109  {
110  int firstVisibleLine;
111  int lastVisibleLine;
112 
113  if (Orientation == Orientation.Vertical)
114  {
115  var itemHeightSpace = (int)Math.Ceiling(MinimumItemSpacing + ItemSlotSize.Height);
116  firstVisibleLine = (int)Math.Ceiling(offset.Y) / itemHeightSpace;
117  lastVisibleLine = firstVisibleLine + (int)Math.Ceiling(panelSize.Height) / itemHeightSpace + 1;
118  }
119  else
120  {
121  var itemWidthSpace = (int)Math.Ceiling(MinimumItemSpacing + ItemSlotSize.Width);
122  firstVisibleLine = (int)Math.Ceiling(offset.X) / itemWidthSpace;
123  lastVisibleLine = firstVisibleLine + (int)Math.Ceiling(panelSize.Width) / itemWidthSpace + 1;
124  }
125 
126  firstVisibleItemIndex = firstVisibleLine * itemsPerLine;
127  lastVisibleItemIndex = lastVisibleLine * itemsPerLine + itemsPerLine - 1;
128 
129  if (lastVisibleItemIndex >= itemCount)
130  lastVisibleItemIndex = itemCount - 1;
131  }
132 
133  /// <inheritdoc/>
134  protected override Size MeasureOverride(Size availableSize)
135  {
136  // initialization side effect happens in the getter of the Children property
137  // and is required for generator to be instanced properly, see below article for reference:
138  // http://stackoverflow.com/questions/3289116/why-does-itemcontainergenerator-return-null
139  // ReSharper disable once UnusedVariable
140  var doNotRemove = Children;
141 
142  itemCount = -1;
143 
144  IItemContainerGenerator generator = ItemContainerGenerator;
145  if (generator == null)
146  return base.MeasureOverride(availableSize);
147 
148  var parentItemsControl = ItemsControl.GetItemsOwner(this);
149  if (parentItemsControl == null)
150  return base.MeasureOverride(availableSize);
151 
152  itemCount = parentItemsControl.Items.Count;
153  itemsPerLine = itemCount;
154  lineCount = itemsPerLine;
155 
156  Size desiredSize;
157  if (Orientation == Orientation.Vertical)
158  {
159  if (double.IsPositiveInfinity(availableSize.Width))
160  throw new InvalidOperationException("Width must not be infinite when virtualizing vertically.");
161 
162  itemsPerLine = (int)Math.Ceiling(availableSize.Width - MinimumItemSpacing) / (int)(ItemSlotSize.Width + MinimumItemSpacing);
163  itemsPerLine = Math.Max(1, Math.Min(itemsPerLine, itemCount));
164  lineCount = ComputeLineCount(itemCount);
165  desiredSize = new Size(availableSize.Width, lineCount * ItemSlotSize.Height);
166  }
167  else
168  {
169  if (double.IsPositiveInfinity(availableSize.Height))
170  throw new InvalidOperationException("Height must not be infinite when virtualizing horizontally.");
171 
172  itemsPerLine = (int)Math.Ceiling(availableSize.Height - MinimumItemSpacing) / (int)Math.Ceiling(ItemSlotSize.Height + MinimumItemSpacing);
173  itemsPerLine = Math.Max(1, Math.Min(itemsPerLine, itemCount));
174  lineCount = ComputeLineCount(itemCount);
175  desiredSize = new Size(lineCount * ItemSlotSize.Width, availableSize.Height);
176  }
177 
178  UpdateScrollInfo(availableSize);
179 
180  int firstVisibleItemIndex, lastVisibleItemIndex;
181  GetVisibilityRange(availableSize, out firstVisibleItemIndex, out lastVisibleItemIndex);
182 
183  // Get the generator position of the first visible data item
184  GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);
185 
186  int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
187 
188  using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
189  {
190  for (int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; itemIndex++, childIndex++)
191  {
192  bool newlyRealized;
193 
194  // Get or create the child
195  var child = generator.GenerateNext(out newlyRealized) as UIElement;
196  if (child == null)
197  continue;
198 
199  if (newlyRealized)
200  {
201  // Figure out if we need to insert the child at the end or somewhere in the middle
202  if (childIndex >= itemCount)
203  AddInternalChild(child);
204  else
205  InsertInternalChild(childIndex, child);
206 
207  generator.PrepareItemContainer(child);
208  }
209 
210  child.Measure(ItemSlotSize);
211  }
212  }
213 
214  CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex);
215 
216  return desiredSize;
217  }
218 
219  /// <inheritdoc/>
220  protected override Size ArrangeOverride(Size finalSize)
221  {
222  // initialization side effect happens in the getter of the Children property
223  // and is required for generator to be instanced properly, see bellow article for reference:
224  // http://stackoverflow.com/questions/3289116/why-does-itemcontainergenerator-return-null
225  // ReSharper disable once UnusedVariable
226  var doNotRemove = Children;
227 
228  IItemContainerGenerator generator = ItemContainerGenerator;
229  if (generator == null)
230  return base.ArrangeOverride(finalSize);
231 
232  var parentItemsControl = ItemsControl.GetItemsOwner(this);
233  if (parentItemsControl == null)
234  return base.ArrangeOverride(finalSize);
235 
236  var space = ComputeItemSpacing(finalSize);
237 
238  for (int i = 0; i < Children.Count; i++)
239  {
240  UIElement child = Children[i];
241 
242  // Map the child offset to an item offset
243  int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
244 
245  if (Orientation == Orientation.Vertical)
246  {
247  int row = itemIndex / itemsPerLine;
248  int column = itemIndex % itemsPerLine;
249 
250  var absoluteY = MinimumItemSpacing + row * (ItemSlotSize.Height + MinimumItemSpacing);
251 
252  child.Arrange(
253  new Rect(
254  new Point(
255  column * (ItemSlotSize.Width + space) - offset.X + MinimumItemSpacing,
256  absoluteY - offset.Y
257  ),
258  ItemSlotSize));
259  }
260  else
261  {
262  int row = itemIndex % itemsPerLine;
263  int column = itemIndex / itemsPerLine;
264 
265  var absoluteX = MinimumItemSpacing + column * (ItemSlotSize.Width + MinimumItemSpacing);
266 
267  child.Arrange(
268  new Rect(
269  new Point(
270  absoluteX - offset.X,
271  row * (ItemSlotSize.Height + space) - offset.Y + MinimumItemSpacing
272  ),
273  ItemSlotSize));
274  }
275  }
276 
277  return finalSize;
278  }
279 
280  private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
281  {
282  UIElementCollection children = InternalChildren;
283  IItemContainerGenerator generator = ItemContainerGenerator;
284 
285  for (int i = children.Count - 1; i >= 0; i--)
286  {
287  var childGeneratorPos = new GeneratorPosition(i, 0);
288  int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
289  if (itemIndex >= 0 && (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated))
290  {
291  generator.Remove(childGeneratorPos, 1);
292  RemoveInternalChildRange(i, 1);
293  }
294  }
295  }
296 
297  private double ComputeItemSpacing(Size finalSize)
298  {
299  double itemsInnerSpace;
300 
301  if (Orientation == Orientation.Vertical)
302  {
303  var totalItemWidth = itemsPerLine * ItemSlotSize.Width;
304  itemsInnerSpace = Math.Max(0.0, finalSize.Width - totalItemWidth - 2 * MinimumItemSpacing);
305  }
306  else
307  {
308  var totalItemHeight = itemsPerLine * ItemSlotSize.Height;
309  itemsInnerSpace = Math.Max(0.0, finalSize.Height - totalItemHeight - 2 * MinimumItemSpacing);
310  }
311 
312  var intervalCount = itemsPerLine - 1;
313  if (intervalCount <= 0)
314  return MinimumItemSpacing;
315 
316  return Math.Max(MinimumItemSpacing, Math.Min(itemsInnerSpace / intervalCount, MaximumItemSpacing));
317  }
318 
319  private void UpdateScrollInfo(Size availableSize)
320  {
321  Size localExtent;
322 
323  if (Orientation == Orientation.Vertical)
324  {
325  localExtent = new Size(
326  Math.Max(availableSize.Width, 2.0 * MinimumItemSpacing + ItemSlotSize.Width),
327  MinimumItemSpacing + lineCount * (ItemSlotSize.Height + MinimumItemSpacing));
328  }
329  else
330  {
331  localExtent = new Size(
332  MinimumItemSpacing + lineCount * (ItemSlotSize.Width + MinimumItemSpacing),
333  Math.Max(availableSize.Height, 2.0 * MinimumItemSpacing + ItemSlotSize.Height));
334  }
335 
336  // update extent
337  if (localExtent != extent)
338  {
339  extent = localExtent;
340  if (ScrollOwner != null)
341  ScrollOwner.InvalidateScrollInfo();
342 
343  Dispatcher.CurrentDispatcher.BeginInvoke((Action)InvalidateMeasure);
344 
345  SetHorizontalOffset(offset.X);
346  SetVerticalOffset(offset.Y);
347  }
348 
349  // update viewport
350  if (availableSize != viewport)
351  {
352  viewport = availableSize;
353 
354  if (ScrollOwner != null)
355  ScrollOwner.InvalidateScrollInfo();
356 
357  SetHorizontalOffset(offset.X);
358  SetVerticalOffset(offset.Y);
359  }
360  }
361 
362  private int ComputeLineCount(int totalItemCount)
363  {
364  return (totalItemCount + itemsPerLine - 1) / itemsPerLine;
365  }
366 
367  public bool CanHorizontallyScroll
368  {
369  get;
370  set;
371  }
372 
373  public bool CanVerticallyScroll
374  {
375  get;
376  set;
377  }
378 
379  private Size extent;
380 
381  public double ExtentWidth
382  {
383  get { return extent.Width; }
384  }
385 
386  public double ExtentHeight
387  {
388  get { return extent.Height; }
389  }
390 
391  public void LineUp()
392  {
393  SetVerticalOffset(VerticalOffset - ItemSlotSize.Height);
394  }
395 
396  public void LineDown()
397  {
398  SetVerticalOffset(VerticalOffset + ItemSlotSize.Height);
399  }
400 
401  public void LineLeft()
402  {
403  SetHorizontalOffset(HorizontalOffset - ItemSlotSize.Width);
404  }
405 
406  public void LineRight()
407  {
408  SetHorizontalOffset(HorizontalOffset + ItemSlotSize.Width);
409  }
410 
411  public void ScrollToIndexedItem(int index)
412  {
413  BringIndexIntoView(index);
414  }
415 
416  protected override void BringIndexIntoView(int index)
417  {
418  base.BringIndexIntoView(index);
419 
420  int n = index / itemsPerLine;
421 
422  var space = ComputeItemSpacing(RenderSize);
423 
424  if (Orientation == Orientation.Vertical)
425  {
426  double newTop = n * (ItemSlotSize.Height + space);
427 
428  if (newTop < offset.Y)
429  SetVerticalOffset(newTop);
430  else if (newTop + ItemSlotSize.Height + space > offset.Y + viewport.Height)
431  SetVerticalOffset(newTop + ItemSlotSize.Height + space - viewport.Height);
432  }
433  else
434  {
435  double newLeft = n * (ItemSlotSize.Width + space);
436 
437  if (newLeft < offset.X)
438  SetHorizontalOffset(newLeft);
439  else if (newLeft + ItemSlotSize.Width + space > offset.X + viewport.Width)
440  SetHorizontalOffset(newLeft + ItemSlotSize.Width + space - viewport.Width);
441  }
442 
443  InvalidateMeasure();
444  }
445 
446  public Rect MakeVisible(Visual visual, Rect rectangle)
447  {
448  if (visual == null)
449  return new Rect();
450 
451  var parentItemsControl = ItemsControl.GetItemsOwner(this);
452  if (parentItemsControl == null)
453  return new Rect();
454 
455  IItemContainerGenerator generator = ItemContainerGenerator;
456 
457  for (int i = 0; i < InternalChildren.Count; i++)
458  {
459  if (Equals(InternalChildren[i], visual))
460  {
461  int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
462 
463  int row = itemIndex / itemsPerLine;
464  int column = itemIndex % itemsPerLine;
465 
466  return new Rect(new Point(column * ItemSlotSize.Width, row * ItemSlotSize.Height), ItemSlotSize);
467  }
468  }
469 
470  return new Rect();
471  }
472 
473  public void MouseWheelUp()
474  {
475  SetVerticalOffset(VerticalOffset - ItemSlotSize.Height);
476  }
477 
478  public void MouseWheelDown()
479  {
480  SetVerticalOffset(VerticalOffset + ItemSlotSize.Height);
481  }
482 
483  public void MouseWheelLeft()
484  {
485  SetHorizontalOffset(HorizontalOffset - ItemSlotSize.Width);
486  }
487 
488  public void MouseWheelRight()
489  {
490  SetHorizontalOffset(HorizontalOffset + ItemSlotSize.Width);
491  }
492 
493  public void PageUp()
494  {
495  SetVerticalOffset(VerticalOffset - viewport.Height);
496  }
497 
498  public void PageDown()
499  {
500  SetVerticalOffset(VerticalOffset + viewport.Height);
501  }
502 
503  public void PageLeft()
504  {
505  SetHorizontalOffset(HorizontalOffset - viewport.Width);
506  }
507 
508  public void PageRight()
509  {
510  SetHorizontalOffset(HorizontalOffset + viewport.Width);
511  }
512 
513  public ScrollViewer ScrollOwner
514  {
515  get;
516  set;
517  }
518 
519  private Point offset;
520 
521  public void SetHorizontalOffset(double horizontalOffset)
522  {
523  if (horizontalOffset < 0.0 || viewport.Width >= extent.Width)
524  horizontalOffset = 0.0;
525  else
526  {
527  if (horizontalOffset + viewport.Width >= extent.Width)
528  horizontalOffset = extent.Width - viewport.Width;
529  }
530 
531  offset.X = horizontalOffset;
532 
533  if (ScrollOwner != null)
534  ScrollOwner.InvalidateScrollInfo();
535 
536  InvalidateMeasure();
537  }
538 
539  public void SetVerticalOffset(double verticalOffset)
540  {
541  if (verticalOffset < 0.0 || viewport.Height >= extent.Height)
542  verticalOffset = 0.0;
543  else
544  {
545  if (verticalOffset + viewport.Height >= extent.Height)
546  verticalOffset = extent.Height - viewport.Height;
547  }
548 
549  offset.Y = verticalOffset;
550 
551  if (ScrollOwner != null)
552  ScrollOwner.InvalidateScrollInfo();
553 
554  InvalidateMeasure();
555  }
556 
557  public double HorizontalOffset
558  {
559  get { return offset.X; }
560  }
561 
562  public double VerticalOffset
563  {
564  get { return offset.Y; }
565  }
566 
567  private Size viewport;
568 
569  public double ViewportHeight
570  {
571  get { return viewport.Height; }
572  }
573 
574  public double ViewportWidth
575  {
576  get { return viewport.Width; }
577  }
578 
579  protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
580  {
581  if (args.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
582  {
583  int index = args.Position.Index;
584  if (args.Position.Offset > 0)
585  {
586  index++;
587  }
588  if (index < InternalChildren.Count && args.ItemUICount > 0)
589  {
590  RemoveInternalChildRange(index, args.ItemUICount);
591  }
592  }
593  base.OnItemsChanged(sender, args);
594  }
595  }
596 }
void GetVisibilityRange(Size panelSize, out int firstVisibleItemIndex, out int lastVisibleItemIndex)
override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
This class describes a Panel similar to a WrapPanel except that items have a reserved space that is e...
Android.Widget.Orientation Orientation
Definition: Section.cs:9
_In_ size_t _In_ size_t size
Definition: DirectXTexP.h:175
System.Windows.Point Point
Definition: ColorPicker.cs:15