// -------------------------------------------------------------------------------------------------------------------- // // 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(); } } } }