// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) 2014 OxyPlot contributors
//
//
// Represents a control that displays a .
//
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.Windows
{
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using global::Windows.ApplicationModel;
using global::Windows.ApplicationModel.DataTransfer;
using global::Windows.Devices.Input;
using global::Windows.Foundation;
using global::Windows.System;
using global::Windows.UI.Core;
using global::Windows.UI.Xaml;
using global::Windows.UI.Xaml.Controls;
using global::Windows.UI.Xaml.Input;
using global::Windows.UI.Xaml.Media.Imaging;
///
/// Represents a control that displays a .
///
[TemplatePart(Name = PartGrid, Type = typeof(Grid))]
public class PlotView : Control, IPlotView
{
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ControllerProperty =
DependencyProperty.Register("Controller", typeof(IPlotController), typeof(PlotView), new PropertyMetadata(null));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty DefaultTrackerTemplateProperty =
DependencyProperty.Register(
"DefaultTrackerTemplate", typeof(ControlTemplate), typeof(PlotView), new PropertyMetadata(null));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty HandleRightClicksProperty =
DependencyProperty.Register("HandleRightClicks", typeof(bool), typeof(PlotView), new PropertyMetadata(true));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty IsMouseWheelEnabledProperty =
DependencyProperty.Register("IsMouseWheelEnabled", typeof(bool), typeof(PlotView), new PropertyMetadata(true));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ModelProperty = DependencyProperty.Register(
"Model", typeof(PlotModel), typeof(PlotView), new PropertyMetadata(null, ModelChanged));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ZoomRectangleTemplateProperty =
DependencyProperty.Register(
"ZoomRectangleTemplate", typeof(ControlTemplate), typeof(PlotView), new PropertyMetadata(null));
///
/// Flags if the cursor is not implemented (Windows Phone).
///
private static bool cursorNotImplemented;
///
/// The Grid PART constant.
///
private const string PartGrid = "PART_Grid";
///
/// The model lock.
///
private readonly object modelLock = new object();
///
/// The tracker definitions.
///
private readonly ObservableCollection trackerDefinitions;
///
/// The canvas.
///
private Canvas canvas;
///
/// The current model.
///
private PlotModel currentModel;
///
/// The current tracker.
///
private FrameworkElement currentTracker;
///
/// The grid.
///
private Grid grid;
///
/// The default controller.
///
private IPlotController defaultController;
///
/// The state of the Alt key.
///
private bool isAltPressed;
///
/// The state of the Windows key.
///
private bool isWindowsPressed;
///
/// The state of the Control key.
///
private bool isControlPressed;
///
/// The is PlotView invalidated.
///
private int isPlotInvalidated;
///
/// The is shift pressed.
///
private bool isShiftPressed;
///
/// The overlays.
///
private Canvas overlays;
///
/// The render context
///
private RenderContext renderContext;
///
/// The zoom control.
///
private ContentControl zoomRectangle;
///
/// Initializes a new instance of the class.
///
public PlotView()
{
this.DefaultStyleKey = typeof(PlotView);
this.trackerDefinitions = new ObservableCollection();
this.Loaded += this.OnLoaded;
this.SizeChanged += this.OnSizeChanged;
this.ManipulationMode = ManipulationModes.Scale | ManipulationModes.TranslateX
| ManipulationModes.TranslateY;
}
///
/// Gets or sets the PlotView controller.
///
/// The PlotView controller.
public IPlotController Controller
{
get { return (IPlotController)this.GetValue(ControllerProperty); }
set { this.SetValue(ControllerProperty, value); }
}
///
/// Gets or sets the default tracker template.
///
public ControlTemplate DefaultTrackerTemplate
{
get
{
return (ControlTemplate)this.GetValue(DefaultTrackerTemplateProperty);
}
set
{
this.SetValue(DefaultTrackerTemplateProperty, value);
}
}
///
/// Gets or sets a value indicating whether to handle right clicks.
///
public bool HandleRightClicks
{
get
{
return (bool)this.GetValue(HandleRightClicksProperty);
}
set
{
this.SetValue(HandleRightClicksProperty, value);
}
}
///
/// Gets or sets a value indicating whether IsMouseWheelEnabled.
///
public bool IsMouseWheelEnabled
{
get
{
return (bool)this.GetValue(IsMouseWheelEnabledProperty);
}
set
{
this.SetValue(IsMouseWheelEnabledProperty, value);
}
}
///
/// Gets or sets the to show.
///
/// The .
public PlotModel Model
{
get
{
return (PlotModel)this.GetValue(ModelProperty);
}
set
{
this.SetValue(ModelProperty, value);
}
}
///
/// Gets or sets the zoom rectangle template.
///
/// The zoom rectangle template.
public ControlTemplate ZoomRectangleTemplate
{
get
{
return (ControlTemplate)this.GetValue(ZoomRectangleTemplateProperty);
}
set
{
this.SetValue(ZoomRectangleTemplateProperty, value);
}
}
///
/// Gets the tracker definitions.
///
/// The tracker definitions.
public ObservableCollection TrackerDefinitions
{
get
{
return this.trackerDefinitions;
}
}
///
/// Gets the actual model in the view.
///
///
/// The actual model.
///
Model IView.ActualModel
{
get
{
return this.Model;
}
}
///
/// Gets the actual model.
///
/// The actual model.
public PlotModel ActualModel
{
get
{
return this.currentModel;
}
}
///
/// Gets the actual controller.
///
///
/// The actual .
///
IController IView.ActualController
{
get
{
return this.ActualController;
}
}
///
/// Gets the coordinates of the client area of the view.
///
public OxyRect ClientArea
{
get
{
return new OxyRect(0, 0, this.ActualWidth, this.ActualHeight);
}
}
///
/// Gets the actual PlotView controller.
///
/// The actual PlotView controller.
public IPlotController ActualController
{
get
{
return this.Controller ?? (this.defaultController ?? (this.defaultController = new PlotController()));
}
}
///
/// Hides the tracker.
///
public void HideTracker()
{
if (this.currentTracker != null)
{
this.overlays.Children.Remove(this.currentTracker);
this.currentTracker = null;
}
}
///
/// Hides the zoom rectangle.
///
public void HideZoomRectangle()
{
this.zoomRectangle.Visibility = Visibility.Collapsed;
}
///
/// Invalidate the PlotView (not blocking the UI thread)
///
/// if set to true, the data collections will be updated.
public void InvalidatePlot(bool update = true)
{
this.UpdateModel(update);
if (DesignMode.DesignModeEnabled)
{
this.InvalidateArrange();
return;
}
if (Interlocked.CompareExchange(ref this.isPlotInvalidated, 1, 0) == 0)
{
// Invalidate the arrange state for the element.
// After the invalidation, the element will have its layout updated,
// which will occur asynchronously unless subsequently forced by UpdateLayout.
this.BeginInvoke(this.InvalidateArrange);
}
}
///
/// Sets the cursor.
///
/// The cursor.
public void SetCursorType(CursorType cursor)
{
if (cursorNotImplemented)
{
// setting the cursor has failed in a previous attempt, see code below
return;
}
var type = CoreCursorType.Arrow;
switch (cursor)
{
case CursorType.Default:
type = CoreCursorType.Arrow;
break;
case CursorType.Pan:
type = CoreCursorType.Hand;
break;
case CursorType.ZoomHorizontal:
type = CoreCursorType.SizeWestEast;
break;
case CursorType.ZoomVertical:
type = CoreCursorType.SizeNorthSouth;
break;
case CursorType.ZoomRectangle:
type = CoreCursorType.SizeNorthwestSoutheast;
break;
}
// TODO: determine if creating a CoreCursor is possible, do not use exception
try
{
var newCursor = new CoreCursor(type, 1); // this line throws an exception on Windows Phone
Window.Current.CoreWindow.PointerCursor = newCursor;
}
catch (NotImplementedException)
{
cursorNotImplemented = true;
}
}
///
/// Shows the tracker.
///
/// The tracker data.
public void ShowTracker(TrackerHitResult trackerHitResult)
{
if (trackerHitResult == null)
{
this.HideTracker();
return;
}
var trackerTemplate = this.DefaultTrackerTemplate;
if (trackerHitResult.Series != null && !string.IsNullOrEmpty(trackerHitResult.Series.TrackerKey))
{
var match = this.TrackerDefinitions.FirstOrDefault(t => t.TrackerKey == trackerHitResult.Series.TrackerKey);
if (match != null)
{
trackerTemplate = match.TrackerTemplate;
}
}
if (trackerTemplate == null)
{
this.HideTracker();
return;
}
var tracker = new ContentControl { Template = trackerTemplate };
if (tracker != this.currentTracker)
{
this.HideTracker();
this.overlays.Children.Add(tracker);
this.currentTracker = tracker;
}
if (this.currentTracker != null)
{
this.currentTracker.DataContext = trackerHitResult;
}
}
///
/// Shows the zoom rectangle.
///
/// The rectangle.
public void ShowZoomRectangle(OxyRect r)
{
this.zoomRectangle.Width = r.Width;
this.zoomRectangle.Height = r.Height;
Canvas.SetLeft(this.zoomRectangle, r.Left);
Canvas.SetTop(this.zoomRectangle, r.Top);
this.zoomRectangle.Template = this.ZoomRectangleTemplate;
this.zoomRectangle.Visibility = Visibility.Visible;
}
///
/// Renders the PlotView to a bitmap.
///
/// A bitmap.
public WriteableBitmap ToBitmap()
{
throw new NotImplementedException();
// var bmp = new RenderTargetBitmap(
// (int)this.ActualWidth, (int)this.ActualHeight, 96, 96, PixelFormats.Pbgra32);
// bmp.Render(this);
// return bmp;
}
///
/// Stores text on the clipboard.
///
/// The text.
void IPlotView.SetClipboardText(string text)
{
var pkg = new DataPackage();
pkg.SetText(text);
// TODO: Clipboard.SetContent(pkg);
}
///
/// Invoked whenever application code or internal processes (such as a rebuilding layout pass) call ApplyTemplate. In simplest terms, this means the method is called just before a UI element displays in your app. Override this method to influence the default post-template logic of a class.
///
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.grid = this.GetTemplateChild(PartGrid) as Grid;
if (this.grid == null)
{
return;
}
this.canvas = new Canvas { IsHitTestVisible = false };
this.grid.Children.Add(this.canvas);
this.canvas.UpdateLayout();
this.renderContext = new RenderContext(this.canvas);
this.overlays = new Canvas();
this.grid.Children.Add(this.overlays);
this.zoomRectangle = new ContentControl();
this.overlays.Children.Add(this.zoomRectangle);
}
///
/// Called before the KeyDown event occurs.
///
/// The data for the event.
protected override void OnKeyDown(KeyRoutedEventArgs e)
{
switch (e.Key)
{
case VirtualKey.Control:
this.isControlPressed = true;
break;
case VirtualKey.Shift:
this.isShiftPressed = true;
break;
case VirtualKey.Menu:
this.isAltPressed = true;
break;
case VirtualKey.LeftWindows:
case VirtualKey.RightWindows:
this.isWindowsPressed = true;
break;
}
var modifiers = OxyModifierKeys.None;
if (this.isControlPressed)
{
modifiers |= OxyModifierKeys.Control;
}
if (this.isAltPressed)
{
modifiers |= OxyModifierKeys.Control;
}
if (this.isShiftPressed)
{
modifiers |= OxyModifierKeys.Shift;
}
if (this.isWindowsPressed)
{
modifiers |= OxyModifierKeys.Windows;
}
if (e.Handled)
{
return;
}
var args = new OxyKeyEventArgs
{
Key = e.Key.Convert(),
ModifierKeys = modifiers,
};
e.Handled = this.ActualController.HandleKeyDown(this, args);
}
///
/// Called before the KeyUp event occurs.
///
/// The data for the event.
protected override void OnKeyUp(KeyRoutedEventArgs e)
{
base.OnKeyUp(e);
switch (e.Key)
{
case VirtualKey.Control:
this.isControlPressed = false;
break;
case VirtualKey.Shift:
this.isShiftPressed = false;
break;
case VirtualKey.Menu:
this.isAltPressed = false;
break;
case VirtualKey.LeftWindows:
case VirtualKey.RightWindows:
this.isWindowsPressed = false;
break;
}
}
///
/// Called before the ManipulationStarted event occurs.
///
/// Event data for the event.
protected override void OnManipulationStarted(ManipulationStartedRoutedEventArgs e)
{
base.OnManipulationStarted(e);
if (e.Handled)
{
return;
}
if (e.PointerDeviceType == PointerDeviceType.Touch)
{
this.Focus(FocusState.Pointer);
e.Handled = this.ActualController.HandleTouchStarted(this, e.ToTouchEventArgs(this));
}
}
///
/// Called before the ManipulationDelta event occurs.
///
/// Event data for the event.
protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
{
base.OnManipulationDelta(e);
if (e.Handled)
{
return;
}
if (e.PointerDeviceType == PointerDeviceType.Touch)
{
e.Handled = this.ActualController.HandleTouchDelta(this, e.ToTouchEventArgs(this));
}
}
///
/// Called before the ManipulationCompleted event occurs.
///
/// Event data for the event.
protected override void OnManipulationCompleted(ManipulationCompletedRoutedEventArgs e)
{
base.OnManipulationCompleted(e);
if (e.Handled)
{
return;
}
if (e.PointerDeviceType == PointerDeviceType.Touch)
{
e.Handled = this.ActualController.HandleTouchCompleted(this, e.ToTouchEventArgs(this));
}
}
///
/// Called before the PointerPressed event occurs.
///
/// Event data for the event.
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
base.OnPointerPressed(e);
if (e.Handled)
{
return;
}
if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
{
this.Focus(FocusState.Pointer);
this.CapturePointer(e.Pointer);
e.Handled = this.ActualController.HandleMouseDown(this, e.ToMouseDownEventArgs(this));
}
else if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch)
{
this.Focus(FocusState.Pointer);
e.Handled = this.ActualController.HandleTouchStarted(this, e.ToTouchEventArgs(this));
}
}
///
/// Called before the PointerMoved event occurs.
///
/// Event data for the event.
protected override void OnPointerMoved(PointerRoutedEventArgs e)
{
base.OnPointerMoved(e);
if (e.Handled)
{
return;
}
if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
{
e.Handled = this.ActualController.HandleMouseMove(this, e.ToMouseEventArgs(this));
}
// Note: don't handle touch here, this is also handled when moving over when a touch device
}
///
/// Called before the PointerReleased event occurs.
///
/// Event data for the event.
protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
base.OnPointerReleased(e);
if (e.Handled)
{
return;
}
if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
{
this.ReleasePointerCapture(e.Pointer);
e.Handled = this.ActualController.HandleMouseUp(this, e.ToMouseEventArgs(this));
}
else if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch)
{
e.Handled = this.ActualController.HandleTouchCompleted(this, e.ToTouchEventArgs(this));
}
}
///
/// Called before the PointerWheelChanged event occurs.
///
/// Event data for the event.
protected override void OnPointerWheelChanged(PointerRoutedEventArgs e)
{
base.OnPointerWheelChanged(e);
if (e.Handled || !this.IsMouseWheelEnabled)
{
return;
}
e.Handled = this.ActualController.HandleMouseWheel(this, e.ToMouseWheelEventArgs(this));
}
///
/// Called before the PointerEntered event occurs.
///
/// Event data for the event.
protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
base.OnPointerEntered(e);
if (e.Handled)
{
return;
}
e.Handled = this.ActualController.HandleMouseEnter(this, e.ToMouseEventArgs(this));
}
///
/// Called before the PointerExited event occurs.
///
/// Event data for the event.
protected override void OnPointerExited(PointerRoutedEventArgs e)
{
base.OnPointerExited(e);
if (e.Handled)
{
return;
}
e.Handled = this.ActualController.HandleMouseLeave(this, e.ToMouseEventArgs(this));
}
///
/// A one time condition for update visuals so it is called no matter the state of the control
/// Currently with out this, the plotview on Xamarin Forms UWP does not render until the app's window resizes
///
private bool isUpdateVisualsCalledOnce = false;
///
/// Provides the behavior for the Arrange pass of layout. Classes can override this method to define their own Arrange pass behavior.
///
/// The final area within the parent that this object should use to arrange itself and its children.
/// The actual size that is used after the element is arranged in layout.
protected override Size ArrangeOverride(Size finalSize)
{
if (this.ActualWidth > 0 && this.ActualHeight > 0)
{
if (Interlocked.CompareExchange(ref this.isPlotInvalidated, 0, 1) == 1)
{
this.UpdateVisuals();
}
}
//see summary for isUpdateVisualsCalledOnce
if (!isUpdateVisualsCalledOnce)
{
this.UpdateVisuals();
isUpdateVisualsCalledOnce = true;
}
return base.ArrangeOverride(finalSize);
}
///
/// Called when the property is changed.
///
/// The sender.
/// The instance containing the event data.
private static void ModelChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
((PlotView)sender).OnModelChanged();
}
///
/// Called when the control is loaded.
///
/// The sender.
/// The instance containing the event data.
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Make sure InvalidateArrange is called when the PlotView is invalidated
Interlocked.Exchange(ref this.isPlotInvalidated, 0);
this.InvalidatePlot();
}
///
/// Called when the model is changed.
///
private void OnModelChanged()
{
lock (this.modelLock)
{
if (this.currentModel != null)
{
((IPlotModel)this.currentModel).AttachPlotView(null);
this.currentModel = null;
}
if (this.Model != null)
{
((IPlotModel)this.Model).AttachPlotView(this);
this.currentModel = this.Model;
}
}
this.InvalidatePlot();
}
///
/// Called when the size of the control is changed.
///
/// The sender.
/// The instance containing the event data.
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
this.InvalidatePlot(false);
}
///
/// Updates the model.
///
/// if set to true, the data collections will be updated.
private void UpdateModel(bool update)
{
if (this.ActualModel != null)
{
((IPlotModel)this.ActualModel).Update(update);
}
}
///
/// Updates the visuals.
///
private void UpdateVisuals()
{
if (this.canvas == null || this.renderContext == null)
{
return;
}
// Clear the canvas
this.canvas.Children.Clear();
if (this.ActualModel != null && !this.ActualModel.Background.IsUndefined())
{
this.canvas.Background = this.ActualModel.Background.ToBrush();
}
else
{
this.canvas.Background = null;
}
if (this.ActualModel != null)
{
((IPlotModel)this.ActualModel).Render(this.renderContext, this.canvas.ActualWidth, this.canvas.ActualHeight);
}
}
///
/// Invokes the specified action on the UI Thread (without blocking the calling thread).
///
/// The action.
private void BeginInvoke(Action action)
{
if (!this.Dispatcher.HasThreadAccess)
{
// TODO: Fix warning?
// Because this call is not awaited, execution of the current method continues before the call is completed.
// Consider applying the 'await' operator to the result of the call.
#pragma warning disable 4014
this.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => action());
#pragma warning restore 4014
}
else
{
action();
}
}
}
}