1 //----------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 //----------------------------------------------------------------
5 //#define MINIMAP_DEBUG
8 namespace System.Activities.Presentation
12 using System.Windows.Controls;
13 using System.Windows.Input;
14 using System.Windows.Media;
15 using System.Windows.Shapes;
16 using System.Diagnostics;
17 using System.Windows.Threading;
18 using System.Globalization;
20 // This class is a control displaying minimap of the attached scrollableview control
21 // this class's functionality is limited to delegating events to minimap view controller
23 partial class MiniMapControl : UserControl
25 public static readonly DependencyProperty MapSourceProperty =
26 DependencyProperty.Register("MapSource",
28 typeof(MiniMapControl),
29 new FrameworkPropertyMetadata(null,
30 FrameworkPropertyMetadataOptions.AffectsRender,
31 new PropertyChangedCallback(OnMapSourceChanged)));
33 MiniMapViewController lookupWindowManager;
34 bool isMouseDown = false;
36 public MiniMapControl()
38 InitializeComponent();
39 this.lookupWindowManager = new MiniMapViewController(this.lookupCanvas, this.lookupWindow, this.contentGrid);
42 public ScrollViewer MapSource
44 get { return GetValue(MapSourceProperty) as ScrollViewer; }
45 set { SetValue(MapSourceProperty, value); }
48 static void OnMapSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
50 MiniMapControl mapControl = (MiniMapControl)sender;
51 mapControl.lookupWindowManager.MapSource = mapControl.MapSource;
54 protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
56 base.OnMouseLeftButtonDown(e);
57 if (this.lookupWindowManager.StartMapLookupDrag(e))
60 this.isMouseDown = true;
64 protected override void OnMouseMove(MouseEventArgs e)
69 this.lookupWindowManager.DoMapLookupDrag(e);
73 protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
75 base.OnMouseLeftButtonUp(e);
79 this.isMouseDown = false;
80 this.lookupWindowManager.StopMapLookupDrag();
84 protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
86 this.lookupWindowManager.CenterView(e);
88 base.OnMouseDoubleClick(e);
91 protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
93 base.OnRenderSizeChanged(sizeInfo);
94 this.lookupWindowManager.MapViewSizeChanged(sizeInfo);
97 // This class wraps positioning and calculating logic of the map view lookup window
98 // It is also responsible for handling mouse movements
100 internal class LookupWindow
103 Rectangle lookupWindowRectangle;
104 MiniMapViewController parent;
107 public LookupWindow(MiniMapViewController parent, Rectangle lookupWindowRectangle)
109 this.mousePosition = new Point();
110 this.parent = parent;
111 this.lookupWindowRectangle = lookupWindowRectangle;
116 get { return Canvas.GetLeft(this.lookupWindowRectangle); }
119 //check if left corner is within minimap's range - clip if necessary
120 double left = Math.Max(value - this.mousePosition.X, 0.0);
121 //check if right corner is within minimap's range - clip if necessary
122 left = (left + Width > this.parent.MapWidth ? this.parent.MapWidth - Width : left);
124 Canvas.SetLeft(this.lookupWindowRectangle, left);
130 get { return Canvas.GetTop(this.lookupWindowRectangle); }
133 //check if top corner is within minimap's range - clip if necessary
134 double top = Math.Max(value - this.mousePosition.Y, 0.0);
135 //check if bottom corner is within minimap's range - clip if necessary
136 top = (top + Height > this.parent.MapHeight ? this.parent.MapHeight - Height : top);
138 Canvas.SetTop(this.lookupWindowRectangle, top);
144 get { return this.lookupWindowRectangle.Width; }
145 set { this.lookupWindowRectangle.Width = value; }
150 get { return this.lookupWindowRectangle.Height; }
151 set { this.lookupWindowRectangle.Height = value; }
154 public double MapCenterXPoint
156 get { return this.Left + (this.Width / 2.0); }
159 public double MapCenterYPoint
161 get { return this.Top + (this.Height / 2.0); }
164 public double MousePositionX
166 get { return this.mousePosition.X; }
169 public double MousePositionY
171 get { return this.mousePosition.Y; }
174 public bool IsSelected
180 public void SetPosition(double left, double top)
186 public void SetSize(double width, double height)
192 //whenever user clicks on the minimap, i check if clicked object is
193 //a lookup window - if yes - i store mouse offset within the window
194 //and mark it as selected
195 public bool Select(object clickedItem, Point clickedPosition)
197 if (clickedItem == this.lookupWindowRectangle)
199 this.mousePosition = clickedPosition;
200 this.IsSelected = true;
206 return this.IsSelected;
209 public void Unselect()
211 this.mousePosition.X = 0;
212 this.mousePosition.Y = 0;
213 this.IsSelected = false;
216 public void Center(double x, double y)
218 Left = x - (Width / 2.0);
219 Top = y - (Height / 2.0);
222 public void Refresh(bool unselect)
228 SetPosition(Left, Top);
232 // This class is responsible for calculating size of the minimap's view area, as well as
233 // maintaining the bi directional link between minimap and control beeing visualized.
234 // Whenever minimap's view window position is updated, the control's content is scrolled
235 // to calculated position
236 // Whenever control's content is resized or scrolled, minimap reflects that change in
237 // recalculating view's window size and/or position
239 internal class MiniMapViewController
243 ScrollViewer mapSource;
244 LookupWindow lookupWindow;
246 public MiniMapViewController(Canvas lookupCanvas, Rectangle lookupWindowRectangle, Grid contentGrid)
248 this.lookupWindow = new LookupWindow(this, lookupWindowRectangle);
249 this.lookupCanvas = lookupCanvas;
250 this.contentGrid = contentGrid;
253 public ScrollViewer MapSource
255 get { return this.mapSource; }
258 this.mapSource = value;
259 //calculate view's size and set initial position
260 this.lookupWindow.Unselect();
261 this.CalculateLookupWindowSize();
262 this.lookupWindow.SetPosition(0.0, 0.0);
263 CalculateMapPosition(this.lookupWindow.Left, this.lookupWindow.Top);
264 this.UpdateContentGrid();
266 if (null != this.mapSource && null != this.mapSource.Content && this.mapSource.Content is FrameworkElement)
268 FrameworkElement content = (FrameworkElement)this.mapSource.Content;
269 //hook up for all content size changes - handle them in OnContentSizeChanged method
270 content.SizeChanged += (s, e) =>
272 this.contentGrid.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle,
273 new Action(() => { OnContentSizeChanged(s, e); }));
276 //in case of scroll viewer - there are two different events to handle in one notification:
277 this.mapSource.ScrollChanged += (s, e) =>
279 this.contentGrid.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle,
282 //when user changes scroll position - delegate it to OnMapSourceScrollChange
283 if (0.0 != e.HorizontalChange || 0.0 != e.VerticalChange)
285 OnMapSourceScrollChanged(s, e);
287 //when size of the scroll changes delegate it to OnContentSizeChanged
288 if (0.0 != e.ViewportWidthChange || 0.0 != e.ViewportHeightChange)
290 OnContentSizeChanged(s, e);
294 this.OnMapSourceScrollChanged(this, null);
295 this.OnContentSizeChanged(this, null);
300 //bunch of helper getters - used to increase algorithm readability and provide default
301 //values, always valid values, so no additional divide-by-zero checks are neccessary
303 public double MapWidth
305 get { return this.contentGrid.ActualWidth - 2 * (this.contentGrid.ColumnDefinitions[0].MinWidth); }
308 public double MapHeight
310 get { return this.contentGrid.ActualHeight - 2 * (this.contentGrid.RowDefinitions[0].MinHeight); }
313 internal LookupWindow LookupWindow
315 get { return this.lookupWindow; }
318 double VisibleSourceWidth
320 get { return (null == MapSource || 0.0 == MapSource.ViewportWidth ? 1.0 : MapSource.ViewportWidth); }
323 double VisibleSourceHeight
325 get { return (null == MapSource || 0.0 == MapSource.ViewportHeight ? 1.0 : MapSource.ViewportHeight); }
329 public void CenterView(MouseEventArgs args)
331 Point pt = args.GetPosition(this.lookupCanvas);
332 this.lookupWindow.Unselect();
333 this.lookupWindow.Center(pt.X, pt.Y);
334 CalculateMapPosition(this.lookupWindow.Left, this.lookupWindow.Top);
337 public void MapViewSizeChanged(SizeChangedInfo sizeInfo)
339 this.OnContentSizeChanged(this, EventArgs.Empty);
340 this.lookupWindow.Unselect();
341 this.CalculateLookupWindowSize();
342 if (sizeInfo.WidthChanged && 0.0 != sizeInfo.PreviousSize.Width)
344 this.lookupWindow.Left =
345 this.lookupWindow.Left * (sizeInfo.NewSize.Width / sizeInfo.PreviousSize.Width);
347 if (sizeInfo.HeightChanged && 0.0 != sizeInfo.PreviousSize.Height)
349 this.lookupWindow.Top =
350 this.lookupWindow.Top * (sizeInfo.NewSize.Height / sizeInfo.PreviousSize.Height);
354 public bool StartMapLookupDrag(MouseEventArgs args)
357 HitTestResult hitTest =
358 VisualTreeHelper.HitTest(this.lookupCanvas, args.GetPosition(this.lookupCanvas));
360 if (null != hitTest && null != hitTest.VisualHit)
362 Point clickedPosition = args.GetPosition(hitTest.VisualHit as IInputElement);
363 result = this.lookupWindow.Select(hitTest.VisualHit, clickedPosition);
368 public void StopMapLookupDrag()
370 this.lookupWindow.Unselect();
373 public void DoMapLookupDrag(MouseEventArgs args)
375 if (args.LeftButton == MouseButtonState.Released && this.lookupWindow.IsSelected)
377 this.lookupWindow.Unselect();
379 if (this.lookupWindow.IsSelected)
381 Point to = args.GetPosition(this.lookupCanvas);
382 this.lookupWindow.SetPosition(to.X, to.Y);
383 CalculateMapPosition(
384 to.X - this.lookupWindow.MousePositionX,
385 to.Y - this.lookupWindow.MousePositionY);
389 void CalculateMapPosition(double left, double top)
391 if (null != MapSource && 0 != this.lookupWindow.Width && 0 != this.lookupWindow.Height)
393 MapSource.ScrollToHorizontalOffset((left / this.lookupWindow.Width) * VisibleSourceWidth);
394 MapSource.ScrollToVerticalOffset((top / this.lookupWindow.Height) * VisibleSourceHeight);
398 //this method calculates position of the lookup window on the minimap - it should be triggered when:
399 // - user modifies a scroll position by draggin a scroll bar
400 // - scroll sizes are updated by change of the srcollviewer size
401 // - user drags minimap view - however, in this case no lookup update takes place
402 void OnMapSourceScrollChanged(object sender, ScrollChangedEventArgs e)
404 if (!this.lookupWindow.IsSelected && null != MapSource)
406 this.lookupWindow.Unselect();
407 this.lookupWindow.Left =
408 this.lookupWindow.Width * (MapSource.HorizontalOffset / VisibleSourceWidth);
410 this.lookupWindow.Top =
411 this.lookupWindow.Height * (MapSource.VerticalOffset / VisibleSourceHeight);
413 DumpData("OnMapSourceScrollChange");
416 //this method calculates size and position of the minimap view - it should be triggered when:
418 // - visible size of the scrollviewer (which is map source) changes
419 // - visible size of the minimap control changes
420 void OnContentSizeChanged(object sender, EventArgs e)
422 //get old center point coordinates
423 double centerX = this.lookupWindow.MapCenterXPoint;
424 double centeryY = this.lookupWindow.MapCenterYPoint;
425 //update the minimap itself
426 this.UpdateContentGrid();
428 this.CalculateLookupWindowSize();
429 //try to center around old center points (window may be moved if doesn't fit)
430 this.lookupWindow.Center(centerX, centeryY);
431 DumpData("OnContentSizeChanged");
434 //this method calculates size of the lookup rectangle, based on the visible size of the object,
435 //including current map width
436 void CalculateLookupWindowSize()
438 double width = this.MapWidth;
439 double height = this.MapHeight;
441 if (this.MapSource.ScrollableWidth != 0 && this.MapSource.ExtentWidth != 0)
443 width = (this.MapSource.ViewportWidth / this.MapSource.ExtentWidth) * this.MapWidth;
449 if (this.MapSource.ScrollableHeight != 0 && this.MapSource.ExtentHeight != 0)
451 height = (this.MapSource.ViewportHeight / this.MapSource.ExtentHeight) * this.MapHeight;
453 this.lookupWindow.SetSize(width, height);
456 //this method updates content grid of the minimap - most likely, minimap view will be scaled to fit
457 //the window - so there will be some extra space visible on the left and right sides or above and below actual
458 //mini map view - we don't want lookup rectangle to navigate within that area, since it is not representing
459 //actual view - we increase margins of the minimap to disallow this
460 void UpdateContentGrid()
462 bool resetToDefault = true;
463 if (this.MapSource.ExtentWidth != 0 && this.MapSource.ExtentHeight != 0)
465 //get width to height ratio from map source - we want to display our minimap in the same ratio
466 double widthToHeightRatio = this.MapSource.ExtentWidth / this.MapSource.ExtentHeight;
468 //calculate current width to height ratio on the minimap
469 double height = this.contentGrid.ActualHeight;
470 double width = this.contentGrid.ActualWidth;
471 //ideally - it should be 1 - whole view perfectly fits minimap
472 double minimapWidthToHeightRatio = (height * widthToHeightRatio) / (width > 1.0 ? width : 1.0);
474 //if value is greater than one - we have to reduce height
475 if (minimapWidthToHeightRatio > 1.0)
477 double margin = (height - (height / minimapWidthToHeightRatio)) / 2.0;
479 this.contentGrid.ColumnDefinitions[0].MinWidth = 0.0;
480 this.contentGrid.ColumnDefinitions[2].MinWidth = 0.0;
481 this.contentGrid.RowDefinitions[0].MinHeight = margin;
482 this.contentGrid.RowDefinitions[2].MinHeight = margin;
483 resetToDefault = false;
485 //if value is less than one - we have to reduce width
486 else if (minimapWidthToHeightRatio < 1.0)
488 double margin = (width - (width * minimapWidthToHeightRatio)) / 2.0;
489 this.contentGrid.ColumnDefinitions[0].MinWidth = margin;
490 this.contentGrid.ColumnDefinitions[2].MinWidth = margin;
491 this.contentGrid.RowDefinitions[0].MinHeight = 0.0;
492 this.contentGrid.RowDefinitions[2].MinHeight = 0.0;
493 resetToDefault = false;
496 //perfect match or nothing to display - no need to setup margins
499 this.contentGrid.ColumnDefinitions[0].MinWidth = 0.0;
500 this.contentGrid.ColumnDefinitions[2].MinWidth = 0.0;
501 this.contentGrid.RowDefinitions[0].MinHeight = 0.0;
502 this.contentGrid.RowDefinitions[2].MinHeight = 0.0;
506 [Conditional("MINIMAP_DEBUG")]
507 void DumpData(string prefix)
509 System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} ScrollViewer: EWidth {1}, EHeight {2}, AWidth {3}, AHeight {4}, ViewPortW {5} ViewPortH {6}", prefix, mapSource.ExtentWidth, mapSource.ExtentHeight, mapSource.ActualWidth, mapSource.ActualHeight, mapSource.ViewportWidth, mapSource.ViewportHeight));