[corlib] Avoid unnecessary ephemeron array resizes
[mono.git] / mcs / class / referencesource / System.Activities.Presentation / System.Activities.Presentation / System / Activities / Presentation / View / RubberBandSelector.cs
1 //----------------------------------------------------------------
2 // <copyright company="Microsoft Corporation">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //----------------------------------------------------------------
6
7 namespace System.Activities.Presentation.View
8 {
9     using System;
10     using System.Activities.Presentation.Internal.PropertyEditing;
11     using System.Activities.Presentation.Model;
12     using System.Collections.Generic;
13     using System.Runtime;
14     using System.Windows;
15     using System.Windows.Documents;
16     using System.Windows.Input;
17     using System.Windows.Media;
18     using System.Windows.Shapes;
19
20     internal sealed class RubberBandSelector
21     {
22         private const double RubberBandThickness = 2;
23         private EditingContext context;
24         private Rectangle rubberBand;
25         private List<WorkflowViewElement> views = new List<WorkflowViewElement>();
26
27         public RubberBandSelector(EditingContext context)
28         {
29             this.context = context;
30             this.InitializeRubberBand();
31         }
32
33         public bool IsSelected
34         {
35             get;
36             set;
37         }
38
39         // Relative to the scrollable content
40         private Point StartPoint
41         {
42             get;
43             set;
44         }
45
46         // Relative to the scrollable content
47         private Point EndPoint
48         {
49             get;
50             set;
51         }
52
53         // Relative to the scrollable content
54         private Point MouseDownPointToScreen { get; set; }
55
56         private bool IsSelecting
57         {
58             get
59             {
60                 return this.ExtenstionSurface.Children.Contains(this.rubberBand);
61             }
62         }
63
64         private bool IsReadyForSelecting
65         {
66             get;
67             set;
68         }
69
70         private ExtensionSurface ExtenstionSurface
71         {
72             get
73             {
74                 return this.Designer.wfViewExtensionSurface;
75             }
76         }
77
78         private DesignerView Designer
79         {
80             get
81             {
82                 return this.context.Services.GetService<DesignerView>();
83             }
84         }
85
86         public void RegisterViewElement(WorkflowViewElement view)
87         {
88             if (!this.views.Contains(view))
89             {
90                 this.views.Add(view);
91             }
92         }
93
94         public void UnregisterViewElement(WorkflowViewElement view)
95         {
96             if (this.views.Contains(view))
97             {
98                 this.views.Remove(view);
99             }
100         }
101
102         public void OnScrollViewerMouseLeftButtonDown(MouseButtonEventArgs e)
103         {
104             this.IsSelected = false;
105
106             // Start rubber band selection if left button down is not handled by UI elements other than WorkflowViewElement.
107             if (!e.Handled || this.Designer.ShouldStillAllowRubberBandEvenIfMouseLeftButtonDownIsHandled)
108             {
109                 if (DesignerView.IsMouseInViewport(e, this.Designer.scrollViewer) && !this.IsMouseOnDragHandle(e) && !this.IsMouseOverAdorner(e))
110                 {
111                     this.IsReadyForSelecting = true;
112                     this.StartPoint = e.GetPosition(this.Designer.scrollableContent);
113
114                     this.MouseDownPointToScreen = this.Designer.scrollableContent.PointToScreen(this.StartPoint);
115                     this.EndPoint = this.StartPoint;
116                 }
117             }
118         }
119
120         public void OnScrollViewerMouseMove(MouseEventArgs e)
121         {
122             if (e.LeftButton == MouseButtonState.Pressed && this.IsReadyForSelecting)
123             {
124                 Point position = e.GetPosition(this.Designer.scrollableContent);
125                 Point positionToScreen = this.Designer.scrollableContent.PointToScreen(position);
126                 if (!this.IsSelecting && (Math.Abs(positionToScreen.X - this.MouseDownPointToScreen.X) > SystemParameters.MinimumHorizontalDragDistance ||
127                     Math.Abs(positionToScreen.Y - this.MouseDownPointToScreen.Y) > SystemParameters.MinimumVerticalDragDistance))
128                 {
129                     this.AddRubberBand();
130                     if (!this.Designer.scrollableContent.IsMouseCaptured)
131                     {
132                         this.Designer.scrollableContent.CaptureMouse();
133                     }
134
135                     e.Handled = true;
136                 }
137
138                 if (this.IsSelecting)
139                 {
140                     this.EndPoint = position;
141                     this.UpdateRubberBand();
142                     AutoScrollHelper.AutoScroll(e.GetPosition(this.Designer.scrollViewer), this.Designer.scrollViewer, 0.2);
143                     e.Handled = true;
144                 }
145             }
146         }
147
148         public void OnScrollViewerPreviewMouseLeftButtonUp(MouseEventArgs e)
149         {
150             if (this.IsSelecting)
151             {
152                 this.EndPoint = e.GetPosition(this.Designer.scrollableContent);
153                 this.Select(Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl));
154                 this.IsSelected = true;
155             }
156
157             this.StopSelecting();
158         }
159
160         public void OnScrollViewerMouseLeave()
161         {
162             this.StopSelecting();
163         }
164
165         public void OnScrollViewerEscapeKeyDown()
166         {
167             this.StopSelecting();
168         }
169
170         private static Point ClipPoint(Point point)
171         {
172             // Negative vaule means the top left corner of the rubber band is outside of the viewport. 
173             // We need to clip the rubber band since a very negative value will cause some WPF display issues.
174             // But we use -RubberBandThickness instead of 0 to clip so that the border of the rubber band that should be outside of the viewport is still outside.
175             return new Point(point.X < -RubberBandThickness ? -RubberBandThickness : point.X, point.Y < -RubberBandThickness ? -RubberBandThickness : point.Y);
176         }
177
178         private void StopSelecting()
179         {
180             this.IsReadyForSelecting = false;
181             if (this.IsSelecting)
182             {
183                 this.RemoveRubberBand();
184                 if (this.Designer.scrollableContent.IsMouseCaptured)
185                 {
186                     this.Designer.scrollableContent.ReleaseMouseCapture();
187                 }
188             }
189         }
190
191         private bool IsMouseOnDragHandle(MouseButtonEventArgs e)
192         {
193             HitTestResult result = VisualTreeHelper.HitTest(this.Designer.scrollableContent, e.GetPosition(this.Designer.scrollableContent));
194             if (result != null)
195             {
196                 WorkflowViewElement view = VisualTreeUtils.FindVisualAncestor<WorkflowViewElement>(result.VisualHit);
197                 if (view != null && view.DragHandle != null)
198                 {
199                     GeneralTransform transform = view.DragHandle.TransformToAncestor(this.Designer);
200                     Fx.Assert(transform != null, "transform should not be null");
201                     Point topLeft = transform.Transform(new Point(0, 0));
202                     Point bottomRight = transform.Transform(new Point(view.DragHandle.ActualWidth, view.DragHandle.ActualHeight));
203                     Rect dragHandleRect = new Rect(topLeft, bottomRight);
204                     if (dragHandleRect.Contains(e.GetPosition(this.Designer)))
205                     {
206                         return true;
207                     }
208                 }
209             }
210
211             return false;
212         }
213
214         private bool IsMouseOverAdorner(MouseButtonEventArgs e)
215         {
216             HitTestResult result = VisualTreeHelper.HitTest(this.Designer.scrollViewer, e.GetPosition(this.Designer.scrollViewer));
217             return result != null && VisualTreeUtils.FindVisualAncestor<Adorner>(result.VisualHit) != null;
218         }
219
220         private void AddRubberBand()
221         {
222             if (!this.ExtenstionSurface.Children.Contains(this.rubberBand))
223             {
224                 this.ExtenstionSurface.Children.Add(this.rubberBand);
225             }
226             else
227             {
228                 Fx.Assert(false, "Old rubber band was not correctly removed.");
229             }
230         }
231
232         private void RemoveRubberBand()
233         {
234             if (this.ExtenstionSurface.Children.Contains(this.rubberBand))
235             {
236                 this.ExtenstionSurface.Children.Remove(this.rubberBand);
237             }
238             else
239             {
240                 Fx.Assert(false, "Rubber band was not correctly added.");
241             }
242         }
243
244         private void UpdateRubberBand()
245         {
246             if (this.ExtenstionSurface.Children.Contains(this.rubberBand))
247             {
248                 // Transform the start and end points to be relative to the extension surface by transforming to the common ancestor (DesignerView) first
249                 GeneralTransform transform1 = this.Designer.scrollableContent.TransformToAncestor(this.Designer);
250                 GeneralTransform transform2 = this.Designer.TransformToDescendant(this.ExtenstionSurface);
251                 Point start = ClipPoint(transform2.Transform(transform1.Transform(this.StartPoint)));
252                 Point end = ClipPoint(transform2.Transform(transform1.Transform(this.EndPoint)));
253                 Rect rect = new Rect(start, end);
254                 this.rubberBand.Width = rect.Width;
255                 this.rubberBand.Height = rect.Height;
256                 this.rubberBand.InvalidateVisual();
257                 ExtensionSurface.SetPosition(this.rubberBand, rect.TopLeft);
258             }
259             else
260             {
261                 Fx.Assert(false, "Rubber band was not correctly added.");
262             }
263         }
264
265         private void Select(bool isCtrlKeyDown)
266         {
267             bool isRubberBandEmpty = true;
268             Rect rubberBandRect = new Rect(this.StartPoint, this.EndPoint);
269             List<ModelItem> selectedModelItems = new List<ModelItem>();
270             foreach (WorkflowViewElement view in this.views)
271             {
272                 GeneralTransform transform = view.TransformToAncestor(this.Designer.scrollableContent);
273                 Point location = transform.Transform(new Point(0, 0));
274                 Rect rect = new Rect(location.X, location.Y, view.ActualWidth, view.ActualHeight);
275                 if (rubberBandRect.Contains(rect))
276                 {
277                     isRubberBandEmpty = false;
278                     if (isCtrlKeyDown)
279                     {
280                         Selection.Toggle(this.context, view.ModelItem);
281                     }
282                     else
283                     {
284                         // Make sure the rubber-band selection has the same order
285                         // and keyboard focus as ctrl+click one by one, which 
286                         // 1) model item is added in reverse order
287                         // 2) last model item, which is the first in selection array,
288                         //    gets focus
289                         selectedModelItems.Insert(0, view.ModelItem);
290                     }
291                 }
292             }
293
294             if (selectedModelItems.Count > 0)
295             {
296                 Keyboard.Focus(selectedModelItems[0].View as IInputElement);
297                 this.context.Items.SetValue(new Selection(selectedModelItems));
298             }
299
300             if (isRubberBandEmpty && !isCtrlKeyDown
301                 && this.ShouldClearSelectioinIfNothingSelected())
302             {
303                 this.context.Items.SetValue(new Selection());
304             }
305         }
306
307         private bool ShouldClearSelectioinIfNothingSelected()
308         {
309             Selection curSelection = this.context.Items.GetValue<Selection>();
310             if (curSelection == null || curSelection.SelectionCount == 0)
311             {
312                 return false;
313             }
314
315             // only one ModelItem is selected and the ModelItem is root designer.
316             // do not clear selection
317             if (curSelection.SelectionCount == 1)
318             {
319                 ModelItem item = curSelection.PrimarySelection;
320                 WorkflowViewElement view = item == null ? null : (item.View as WorkflowViewElement);
321                 if (view != null && view.IsRootDesigner)
322                 {
323                     return false;
324                 }
325             }
326
327             return true;
328         }
329
330         private void InitializeRubberBand()
331         {
332             this.rubberBand = new Rectangle();
333             this.rubberBand.StrokeThickness = RubberBandThickness;
334             this.rubberBand.Stroke = WorkflowDesignerColors.RubberBandRectangleBrush;
335             this.rubberBand.Fill = WorkflowDesignerColors.RubberBandRectangleBrush.Clone();
336             this.rubberBand.Fill.Opacity = 0.2;
337         }
338     }
339 }