Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
ListBoxRectangleSelectionBehavior.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.Documents;
8 using System.Windows.Input;
9 using System.Windows.Media;
10 using System.Windows.Threading;
11 using SiliconStudio.Presentation.Extensions;
12 
13 // source: http://www.codeproject.com/Articles/209560/ListBox-drag-selection
14 // license: The Code Project Open License (CPOL) 1.02
15 
16 namespace SiliconStudio.Presentation.Behaviors
17 {
18  public class ListBoxRectangleSelectionBehavior : DeferredBehaviorBase<ListBox>
19  {
20  private ListBox listBox;
21  private FrameworkElement itemsPresenter;
22 
23  private SelectionAdorner selectionRect;
24  private AutoScroller autoScroller;
25  private ItemsControlSelector selector;
26 
27  private bool mouseCaptured;
28  private Point start;
29  private Point end;
30 
31  protected override void OnAttachedOverride()
32  {
33  listBox = AssociatedObject;
34 
35  // If we're enabling selection by a rectangle we can assume
36  // this means we want to be able to select more than one item.
37  if (listBox.SelectionMode == SelectionMode.Single)
38  listBox.SelectionMode = SelectionMode.Extended;
39 
40  Register();
41  }
42 
43  protected override void OnDetachingOverride()
44  {
45  Unregister();
46  }
47 
48  private void Register()
49  {
50  itemsPresenter = listBox.FindVisualChildOfType<ItemsPresenter>();
51 
52  if (itemsPresenter == null)
53  return;
54 
55  var adornerLayer = AdornerLayer.GetAdornerLayer(itemsPresenter);
56  if (adornerLayer == null)
57  return;
58 
59  selectionRect = new SelectionAdorner(itemsPresenter);
60  adornerLayer.Add(selectionRect);
61 
62  selector = new ItemsControlSelector(listBox);
63 
64  autoScroller = new AutoScroller(listBox);
65  autoScroller.OffsetChanged += OnOffsetChanged;
66 
67  // The ListBox intercepts the regular MouseLeftButtonDown event
68  // to do its selection processing, so we need to handle the
69  // PreviewMouseLeftButtonDown. The scroll content won't receive
70  // the message if we click on a blank area so use the ListBox.
71  listBox.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
72  listBox.MouseLeftButtonUp += OnMouseLeftButtonUp;
73  listBox.MouseMove += OnMouseMove;
74  }
75 
76  private void Unregister()
77  {
78  if (selectionRect != null && autoScroller != null)
79  {
80  StopSelection();
81  }
82 
83  // Remove all the event handlers so this instance can be reclaimed by the GC.
84  listBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
85  listBox.MouseLeftButtonUp -= OnMouseLeftButtonUp;
86  listBox.MouseMove -= OnMouseMove;
87 
88  if (autoScroller != null)
89  autoScroller.Unregister();
90  }
91 
92  private Point positionAtMouseDown;
93  private bool isDragging;
94  private bool isSelectedAtMouseDown;
95 
96  private readonly Type[] knownPresenterTypes = new[]
97  {
98  typeof(ContentPresenter),
99  typeof(GridViewRowPresenterBase)
100  };
101 
102  private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
103  {
104  positionAtMouseDown = e.GetPosition(itemsPresenter);
105  isDragging = false;
106  mouseCaptured = false;
107 
108  var item = GetItemAt(listBox, e.GetPosition) as ListBoxItem;
109 
110  isSelectedAtMouseDown = false;
111 
112  if (item != null)
113  {
114  if (item.IsSelected)
115  isSelectedAtMouseDown = true;
116  else
117  {
118  var result = VisualTreeHelper.HitTest(item, e.GetPosition(item));
119  if (result != null && result.VisualHit != null)
120  {
121  var presenter = result.VisualHit.FindVisualParentOfType<FrameworkElement>();
122 
123  if (presenter != null)
124  {
125  var presenterType = presenter.GetType();
126  if (Array.Exists(knownPresenterTypes, t => t.IsAssignableFrom(presenterType)))
127  isSelectedAtMouseDown = true;
128  }
129  }
130  }
131  }
132  }
133 
134  private void OnMouseMove(object sender, MouseEventArgs e)
135  {
136  if (e.LeftButton != MouseButtonState.Pressed)
137  return;
138 
139  if (isDragging == false)
140  {
141  var currentMousePosition = e.GetPosition(itemsPresenter);
142 
143  var dx = currentMousePosition.X - positionAtMouseDown.X;
144  var dy = currentMousePosition.Y - positionAtMouseDown.Y;
145 
146  if (Math.Abs(dx) > SystemParameters.MinimumHorizontalDragDistance ||
147  Math.Abs(dy) > SystemParameters.MinimumVerticalDragDistance)
148  {
149  isDragging = true;
150 
151  if (isSelectedAtMouseDown == false)
152  {
153  if (positionAtMouseDown.X >= 0 && positionAtMouseDown.X < itemsPresenter.ActualWidth &&
154  positionAtMouseDown.Y >= 0 && positionAtMouseDown.Y < itemsPresenter.ActualHeight)
155  {
156  mouseCaptured = TryCaptureMouse(e);
157  if (mouseCaptured)
158  {
159  StartSelection(positionAtMouseDown);
160  e.Handled = true;
161  }
162  }
163  }
164  }
165  }
166 
167  if (mouseCaptured)
168  {
169  // Get the position relative to the content of the ScrollViewer.
170  end = e.GetPosition(itemsPresenter);
171  autoScroller.Update(end);
172  UpdateSelection();
173  }
174  }
175 
176  private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
177  {
178  if (isDragging == false)
179  {
180  var item = GetItemAt(listBox, e.GetPosition) as ListBoxItem;
181  if (item != null)
182  {
183  var isControlPressed = (Keyboard.Modifiers & ModifierKeys.Control) != 0;
184 
185  if (isControlPressed)
186  {
187  // toggle current item selection
188  //item.IsSelected = !isSelectedAtMouseDown;
189 
190  // seems to be already supported by [SelectionMode = SelectionMode.Extended]
191  // doing it again revert selection to bad state, but wasn't the case before 0_o
192  }
193  else
194  {
195  // select current item
196  listBox.SelectedItems.Clear();
197  item.IsSelected = true;
198  }
199  }
200  else
201  {
202  listBox.SelectedItems.Clear();
203  }
204  }
205 
206  isDragging = false;
207 
208  if (mouseCaptured)
209  {
210  mouseCaptured = false;
211  itemsPresenter.ReleaseMouseCapture();
212  StopSelection();
213  }
214  }
215 
216  private void OnOffsetChanged(object sender, OffsetChangedEventArgs e)
217  {
218  selector.Scroll(e.HorizontalChange, e.VerticalChange);
219  UpdateSelection();
220  }
221 
222  private void StartSelection(Point location)
223  {
224  // We've stolen the MouseLeftButtonDown event from the ListBox
225  // so we need to manually give it focus.
226  listBox.Focus();
227 
228  start = location;
229  end = location;
230 
231  // Do we need to start a new selection?
232  if ((Keyboard.Modifiers & ModifierKeys.Control) == 0 &&
233  (Keyboard.Modifiers & ModifierKeys.Shift) == 0)
234  {
235  // Neither the shift key or control key is pressed, so
236  // clear the selection.
237  listBox.SelectedItems.Clear();
238  }
239 
240  selector.Reset();
241  UpdateSelection();
242 
243  selectionRect.IsEnabled = true;
244  autoScroller.IsEnabled = true;
245  }
246 
247  private void UpdateSelection()
248  {
249  // Offset the start point based on the scroll offset.
250  var transStart = autoScroller.TranslatePoint(start);
251 
252  // Draw the selecion rectangle.
253  // Rect can't have a negative width/height...
254  var x = Math.Min(transStart.X, end.X);
255  var y = Math.Min(transStart.Y, end.Y);
256  var width = Math.Abs(end.X - transStart.X);
257  var height = Math.Abs(end.Y - transStart.Y);
258 
259  var area = new Rect(x, y, width, height);
260  selectionRect.SelectionArea = area;
261 
262  // Select the items.
263  // Transform the points to be relative to the ListBox.
264  var topLeft = itemsPresenter.TranslatePoint(area.TopLeft, listBox);
265  var bottomRight = itemsPresenter.TranslatePoint(area.BottomRight, listBox);
266 
267  // And select the items.
268  selector.UpdateSelection(new Rect(topLeft, bottomRight));
269  }
270 
271  private void StopSelection()
272  {
273  // Hide the selection rectangle and stop the auto scrolling.
274  selectionRect.IsEnabled = false;
275  autoScroller.IsEnabled = false;
276  }
277 
278  private FrameworkElement GetItemAt(ItemsControl itemsControl, Func<IInputElement, Point> getPosition)
279  {
280  for (int i = 0; i < itemsControl.Items.Count; i++)
281  {
282  var item = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
283  if (item != null)
284  {
285  var bounds = VisualTreeHelper.GetDescendantBounds(item);
286  if (bounds.Contains(getPosition(item)))
287  return item;
288  }
289  }
290 
291  return null;
292  }
293 
294  private bool TryCaptureMouse(MouseEventArgs e)
295  {
296  var position = e.GetPosition(itemsPresenter);
297 
298  // Check if there is anything under the mouse.
299  var element = itemsPresenter.InputHitTest(position) as UIElement;
300  if (element != null)
301  {
302  // Simulate a mouse click by sending it the MouseButtonDown
303  // event based on the data we received.
304  var args = new MouseButtonEventArgs(
305  e.MouseDevice,
306  e.Timestamp,
307  MouseButton.Left,
308  e.StylusDevice) { RoutedEvent = Mouse.MouseDownEvent, Source = e.Source };
309 
310  element.RaiseEvent(args);
311 
312  // The ListBox will try to capture the mouse unless something
313  // else captures it.
314  if (!ReferenceEquals(Mouse.Captured, listBox))
315  return false; // Something else wanted the mouse, let it keep it.
316  }
317 
318  // Either there's nothing under the mouse or the element doesn't want the mouse.
319  return itemsPresenter.CaptureMouse();
320  }
321 
322  // *** Helper types *****************************************************************************
323 
324  /// <summary>
325  /// Automatically scrolls an ItemsControl when the mouse is dragged outside
326  /// of the control.
327  /// </summary>
328  private sealed class AutoScroller
329  {
330  private readonly DispatcherTimer autoScroll = new DispatcherTimer();
331  private readonly ItemsControl itemsControl;
332  private readonly ScrollViewer scrollViewer;
333  private readonly ScrollContentPresenter scrollContent;
334  private bool isEnabled;
335  private Point offset;
336  private Point mouse;
337 
338  /// <summary>
339  /// Initializes a new instance of the AutoScroller class.
340  /// </summary>
341  /// <param name="itemsControl">The ItemsControl that is scrolled.</param>
342  /// <exception cref="ArgumentNullException">itemsControl is null.</exception>
343  public AutoScroller(ItemsControl itemsControl)
344  {
345  if (itemsControl == null)
346  {
347  throw new ArgumentNullException("itemsControl");
348  }
349 
350  this.itemsControl = itemsControl;
351  scrollViewer = itemsControl.FindVisualChildOfType<ScrollViewer>();
352  scrollViewer.ScrollChanged += OnScrollChanged;
353  scrollContent = scrollViewer.FindVisualChildOfType<ScrollContentPresenter>();
354 
355  autoScroll.Tick += delegate { PreformScroll(); };
356  autoScroll.Interval = TimeSpan.FromMilliseconds(GetRepeatRate());
357  }
358 
359  /// <summary>Occurs when the scroll offset has changed.</summary>
360  public event EventHandler<OffsetChangedEventArgs> OffsetChanged;
361 
362  /// <summary>
363  /// Gets or sets a value indicating whether the auto-scroller is enabled
364  /// or not.
365  /// </summary>
366  public bool IsEnabled
367  {
368  private get
369  {
370  return isEnabled;
371  }
372  set
373  {
374  if (isEnabled != value)
375  {
376  isEnabled = value;
377 
378  // Reset the auto-scroller and offset.
379  autoScroll.IsEnabled = false;
380  offset = new Point();
381  }
382  }
383  }
384 
385  /// <summary>
386  /// Translates the specified point by the current scroll offset.
387  /// </summary>
388  /// <param name="point">The point to translate.</param>
389  /// <returns>A new point offset by the current scroll amount.</returns>
390  public Point TranslatePoint(Point point)
391  {
392  return new Point(point.X - offset.X, point.Y - offset.Y);
393  }
394 
395  /// <summary>
396  /// Removes all the event handlers registered on the control.
397  /// </summary>
398  public void Unregister()
399  {
400  scrollViewer.ScrollChanged -= OnScrollChanged;
401  }
402 
403  /// <summary>
404  /// Updates the location of the mouse and automatically scrolls if required.
405  /// </summary>
406  /// <param name="mousePosition">
407  /// The location of the mouse, relative to the ScrollViewer's content.
408  /// </param>
409  public void Update(Point mousePosition)
410  {
411  mouse = mousePosition;
412 
413  // If scrolling isn't enabled then see if it needs to be.
414  if (autoScroll.IsEnabled == false)
415  {
416  PreformScroll();
417  }
418  }
419 
420  // Returns the default repeat rate in milliseconds.
421  private static int GetRepeatRate()
422  {
423  // The RepeatButton uses the SystemParameters.KeyboardSpeed as the
424  // default value for the Interval property. KeyboardSpeed returns
425  // a value between 0 (400ms) and 31 (33ms).
426  const double Ratio = (400.0 - 33.0) / 31.0;
427  return 400 - (int)(SystemParameters.KeyboardSpeed * Ratio);
428  }
429 
430  private double CalculateOffset(int startIndex, int endIndex)
431  {
432  var sum = 0.0;
433 
434  for (int i = startIndex; i != endIndex; i++)
435  {
436  var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
437  if (container != null)
438  {
439  // Height = Actual height + margin
440  sum += container.ActualHeight;
441  sum += container.Margin.Top + container.Margin.Bottom;
442  }
443  }
444 
445  return sum;
446  }
447 
448  private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
449  {
450  // Do we need to update the offset?
451  if (IsEnabled)
452  {
453  var horizontal = e.HorizontalChange;
454  var vertical = e.VerticalChange;
455 
456  // VerticalOffset means two seperate things based on the CanContentScroll
457  // property. If this property is true then the offset is the number of
458  // items to scroll; false then it's in Device Independant Pixels (DIPs).
459  if (scrollViewer.CanContentScroll)
460  {
461  // We need to either increase the offset or decrease it.
462  if (e.VerticalChange < 0)
463  {
464  var start = (int)e.VerticalOffset;
465  var end = (int)(e.VerticalOffset - e.VerticalChange);
466  vertical = -CalculateOffset(start, end);
467  }
468  else
469  {
470  var start = (int)(e.VerticalOffset - e.VerticalChange);
471  var end = (int)e.VerticalOffset;
472  vertical = CalculateOffset(start, end);
473  }
474  }
475 
476  offset.X += horizontal;
477  offset.Y += vertical;
478 
479  var callback = OffsetChanged;
480  if (callback != null)
481  {
482  callback(this, new OffsetChangedEventArgs(horizontal, vertical));
483  }
484  }
485  }
486 
487  private void PreformScroll()
488  {
489  var scrolled = false;
490 
491  var size = VisualTreeHelper.GetDescendantBounds(scrollViewer);
492 
493  if (mouse.X > size.Width /*scrollContent.ActualWidth*/)
494  {
495  scrollViewer.LineRight();
496  scrolled = true;
497  }
498  else if (mouse.X < 0)
499  {
500  scrollViewer.LineLeft();
501  scrolled = true;
502  }
503 
504  if (mouse.Y > scrollContent.ActualHeight)
505  {
506  scrollViewer.LineDown();
507  scrolled = true;
508  }
509  else if (mouse.Y < 0)
510  {
511  scrollViewer.LineUp();
512  scrolled = true;
513  }
514 
515  // It's important to disable scrolling if we're inside the bounds of
516  // the control so that when the user does leave the bounds we can
517  // re-enable scrolling and it will have the correct initial delay.
518  autoScroll.IsEnabled = scrolled;
519  }
520  }
521 
522  /// <summary>Enables the selection of items by a specified rectangle.</summary>
523  private sealed class ItemsControlSelector
524  {
525  private readonly ItemsControl itemsControl;
526  private Rect previousArea;
527 
528  /// <summary>
529  /// Initializes a new instance of the ItemsControlSelector class.
530  /// </summary>
531  /// <param name="itemsControl">
532  /// The control that contains the items to select.
533  /// </param>
534  /// <exception cref="ArgumentNullException">itemsControl is null.</exception>
535  public ItemsControlSelector(ItemsControl itemsControl)
536  {
537  if (itemsControl == null)
538  throw new ArgumentNullException("itemsControl");
539 
540  this.itemsControl = itemsControl;
541  }
542 
543  /// <summary>
544  /// Resets the cached information, allowing a new selection to begin.
545  /// </summary>
546  public void Reset()
547  {
548  previousArea = new Rect();
549  }
550 
551  /// <summary>
552  /// Scrolls the selection area by the specified amount.
553  /// </summary>
554  /// <param name="x">The horizontal scroll amount.</param>
555  /// <param name="y">The vertical scroll amount.</param>
556  public void Scroll(double x, double y)
557  {
558  previousArea.Offset(-x, -y);
559  }
560 
561  /// <summary>
562  /// Updates the controls selection based on the specified area.
563  /// </summary>
564  /// <param name="area">
565  /// The selection area, relative to the control passed in the contructor.
566  /// </param>
567  public void UpdateSelection(Rect area)
568  {
569  // Check eack item to see if it intersects with the area.
570  for (int i = 0; i < itemsControl.Items.Count; i++)
571  {
572  var item = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
573  if (item != null)
574  {
575  // Get the bounds in the parent's co-ordinates.
576  var topLeft = item.TranslatePoint(new Point(0, 0), itemsControl);
577  var itemBounds = new Rect(topLeft.X, topLeft.Y, item.ActualWidth, item.ActualHeight);
578 
579  // Only change the selection if it intersects with the area
580  // (or intersected i.e. we changed the value last time).
581  if (itemBounds.IntersectsWith(area))
582  {
583  Selector.SetIsSelected(item, true);
584  }
585  else if (itemBounds.IntersectsWith(previousArea))
586  {
587  // We previously changed the selection to true but it no
588  // longer intersects with the area so clear the selection.
589  Selector.SetIsSelected(item, false);
590  }
591  }
592  }
593 
594  previousArea = area;
595  }
596  }
597 
598  /// <summary>The event data for the AutoScroller.OffsetChanged event.</summary>
599  private sealed class OffsetChangedEventArgs : EventArgs
600  {
601  /// <summary>
602  /// Initializes a new instance of the OffsetChangedEventArgs class.
603  /// </summary>
604  /// <param name="horizontal">The change in horizontal scroll.</param>
605  /// <param name="vertical">The change in vertical scroll.</param>
606  internal OffsetChangedEventArgs(double horizontal, double vertical)
607  {
608  HorizontalChange = horizontal;
609  VerticalChange = vertical;
610  }
611 
612  /// <summary>Gets the change in horizontal scroll position.</summary>
613  public double HorizontalChange { get; private set; }
614 
615  /// <summary>Gets the change in vertical scroll position.</summary>
616  public double VerticalChange { get; private set; }
617  }
618 
619  /// <summary>Draws a selection rectangle on an AdornerLayer.</summary>
620  private sealed class SelectionAdorner : Adorner
621  {
622  private Rect selectionRect;
623  private readonly Brush fill;
624  private readonly Pen pen;
625 
626  /// <summary>
627  /// Initializes a new instance of the SelectionAdorner class.
628  /// </summary>
629  /// <param name="parent">
630  /// The UIElement which this instance will overlay.
631  /// </param>
632  /// <exception cref="ArgumentNullException">parent is null.</exception>
633  public SelectionAdorner(UIElement parent)
634  : base(parent)
635  {
636  // Make sure the mouse doesn't see us.
637  IsHitTestVisible = false;
638 
639  fill = SystemColors.HighlightBrush.Clone();
640  fill.Opacity = 0.4;
641  fill.Freeze();
642 
643  pen = new Pen(SystemColors.HighlightBrush, 1.0);
644  pen.Freeze();
645 
646  // We only draw a rectangle when we're enabled.
647  IsEnabledChanged += delegate { InvalidateVisual(); };
648  }
649 
650  /// <summary>Gets or sets the area of the selection rectangle.</summary>
651  public Rect SelectionArea
652  {
653  get
654  {
655  return selectionRect;
656  }
657  set
658  {
659  selectionRect = value;
660  InvalidateVisual();
661  }
662  }
663 
664  /// <summary>
665  /// Participates in rendering operations that are directed by the layout system.
666  /// </summary>
667  /// <param name="drawingContext">The drawing instructions.</param>
668  protected override void OnRender(DrawingContext drawingContext)
669  {
670  base.OnRender(drawingContext);
671 
672  if (IsEnabled)
673  {
674  // Make the lines snap to pixels (add half the pen width [0.5])
675  double[] x = { SelectionArea.Left + 0.5, SelectionArea.Right + 0.5 };
676  double[] y = { SelectionArea.Top + 0.5, SelectionArea.Bottom + 0.5 };
677  drawingContext.PushGuidelineSet(new GuidelineSet(x, y));
678 
679  drawingContext.DrawRectangle(fill, pen, SelectionArea);
680  }
681  }
682  }
683  }
684 }
MouseButton
Mouse buttons.
Definition: MouseButton.cs:10
System.Windows.Media.Colors SystemColors
_In_ size_t _In_ DXGI_FORMAT _In_ size_t _In_ float size_t y
Definition: DirectXTexP.h:191
The validation occurs every time the mouse moves.
A keyboard virtual button.
document false
The device failed due to a badly formed command. This is a run-time issue; The application should des...
_In_ size_t _In_ size_t size
Definition: DirectXTexP.h:175
System.Windows.Point Point
Definition: ColorPicker.cs:15