// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) 2014 OxyPlot contributors // // // Represents a control that displays a . // // -------------------------------------------------------------------------------------------------------------------- using Avalonia.Reactive; namespace OxyPlot.Avalonia { using global::Avalonia; using global::Avalonia.Controls; using global::Avalonia.Controls.Presenters; using global::Avalonia.Controls.Primitives; using global::Avalonia.Input; using global::Avalonia.Threading; using global::Avalonia.VisualTree; using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; /// /// Represents a control that displays a . /// public abstract partial class PlotBase : TemplatedControl, IPlotView { /// /// The Grid PART constant. /// protected const string PartPanel = "PART_Panel"; /// /// The tracker definitions. /// private readonly ObservableCollection trackerDefinitions; /// /// The render context /// private CanvasRenderContext renderContext; /// /// The canvas. /// private Canvas canvas; /// /// The current tracker. /// private Control currentTracker; /// /// The grid. /// private Panel panel; /// /// Invalidation flag (0: no update, 1: update, 2: update date). /// private int isUpdateRequired; /// /// Invalidation flag (0: no update, 1: update visual elements). /// private int isPlotInvalidated; /// /// The mouse down point. /// private ScreenPoint mouseDownPoint; /// /// The overlays. /// private Canvas overlays; /// /// The zoom control. /// private ContentControl zoomControl; /// /// The is visible to user cache. /// private bool isVisibleToUserCache; /// /// The cached parent. /// private Control containerCache; /// /// Initializes a new instance of the class. /// protected PlotBase() { DisconnectCanvasWhileUpdating = true; trackerDefinitions = new ObservableCollection(); this.GetObservable(BoundsProperty).Subscribe(new AnonymousObserver(OnSizeChanged)); } /// /// Gets or sets a value indicating whether to disconnect the canvas while updating. /// /// true if canvas should be disconnected while updating; otherwise, false. public bool DisconnectCanvasWhileUpdating { get; set; } /// /// Gets the actual model in the view. /// /// /// The actual model. /// Model IView.ActualModel { get { return ActualModel; } } /// /// Gets the actual model. /// /// The actual model. public abstract PlotModel ActualModel { get; } /// /// Gets the actual controller. /// /// /// The actual . /// IController IView.ActualController { get { return ActualController; } } /// /// Gets the actual PlotView controller. /// /// The actual PlotView controller. public abstract IPlotController ActualController { get; } /// /// Gets the coordinates of the client area of the view. /// public OxyRect ClientArea { get { return new OxyRect(0, 0, Bounds.Width, Bounds.Height); } } /// /// Gets the tracker definitions. /// /// The tracker definitions. public ObservableCollection TrackerDefinitions { get { return trackerDefinitions; } } /// /// Hides the tracker. /// public void HideTracker() { if (currentTracker != null) { overlays.Children.Remove(currentTracker); currentTracker = null; } } /// /// Hides the zoom rectangle. /// public void HideZoomRectangle() { zoomControl.IsVisible = false; } /// /// Pans all axes. /// /// The delta. public void PanAllAxes(Vector delta) { ActualModel?.PanAllAxes(delta.X, delta.Y); InvalidatePlot(false); } /// /// Zooms all axes. /// /// The zoom factor. public void ZoomAllAxes(double factor) { ActualModel?.ZoomAllAxes(factor); InvalidatePlot(false); } /// /// Resets all axes. /// public void ResetAllAxes() { ActualModel?.ResetAllAxes(); InvalidatePlot(false); } /// /// Invalidate the PlotView (not blocking the UI thread) /// /// The update Data. public void InvalidatePlot(bool updateData = true) { // perform update on UI thread var updateState = updateData ? 2 : 1; int currentState = isUpdateRequired; while (currentState < updateState) { if (Interlocked.CompareExchange(ref isUpdateRequired, updateState, currentState) == currentState) { BeginInvoke(() => UpdateModel(updateData)); break; } else { currentState = isUpdateRequired; } } } /// /// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass) /// call . In simplest terms, this means the method is called /// just before a UI element displays in an application. For more information, see Remarks. /// /// Event data for applying the template. protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); panel = e.NameScope.Find(PartPanel) as Panel; if (panel == null) { return; } canvas = new Canvas(); panel.Children.Add(canvas); renderContext = new CanvasRenderContext(canvas); overlays = new Canvas { Name = "Overlays" }; panel.Children.Add(overlays); zoomControl = new ContentControl(); overlays.Children.Add(zoomControl); } /// /// Sets the cursor type. /// /// The cursor type. public void SetCursorType(CursorType cursorType) { switch (cursorType) { case CursorType.Pan: Cursor = PanCursor; break; case CursorType.ZoomRectangle: Cursor = ZoomRectangleCursor; break; case CursorType.ZoomHorizontal: Cursor = ZoomHorizontalCursor; break; case CursorType.ZoomVertical: Cursor = ZoomVerticalCursor; break; default: Cursor = Cursor.Default; break; } } /// /// Shows the tracker. /// /// The tracker data. public void ShowTracker(TrackerHitResult trackerHitResult) { if (trackerHitResult == null) { HideTracker(); return; } var trackerTemplate = DefaultTrackerTemplate; if (trackerHitResult.Series != null && !string.IsNullOrEmpty(trackerHitResult.Series.TrackerKey)) { var match = TrackerDefinitions.FirstOrDefault(t => t.TrackerKey == trackerHitResult.Series.TrackerKey); if (match != null) { trackerTemplate = match.TrackerTemplate; } } if (trackerTemplate == null) { HideTracker(); return; } var tracker = trackerTemplate.Build(new ContentControl()); // ReSharper disable once RedundantNameQualifier if (!object.ReferenceEquals(tracker, currentTracker)) { HideTracker(); overlays.Children.Add(tracker.Result); currentTracker = tracker.Result; } if (currentTracker != null) { currentTracker.DataContext = trackerHitResult; } } /// /// Shows the zoom rectangle. /// /// The rectangle. public void ShowZoomRectangle(OxyRect r) { zoomControl.Width = r.Width; zoomControl.Height = r.Height; Canvas.SetLeft(zoomControl, r.Left); Canvas.SetTop(zoomControl, r.Top); zoomControl.Template = ZoomRectangleTemplate; zoomControl.IsVisible = true; } /// /// Stores text on the clipboard. /// /// The text. public async void SetClipboardText(string text) { if (TopLevel.GetTopLevel(this) is { Clipboard: { } clipboard }) { await clipboard.SetTextAsync(text).ConfigureAwait(true); } } /// /// Provides the behavior for the Arrange pass of Silverlight 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) { var actualSize = base.ArrangeOverride(finalSize); if (actualSize.Width > 0 && actualSize.Height > 0) { if (Interlocked.CompareExchange(ref isPlotInvalidated, 0, 1) == 1) { UpdateVisuals(); } } return actualSize; } /// /// Updates the model. /// /// The update Data. protected void UpdateModel(bool updateData = true) { if (Width <= 0 || Height <= 0 || ActualModel == null) { isUpdateRequired = 0; return; } lock (this.ActualModel.SyncRoot) { var updateState = (Interlocked.Exchange(ref isUpdateRequired, 0)); if (updateState > 0) { ((IPlotModel)ActualModel).Update(updateState == 2 || updateData); } } if (Interlocked.CompareExchange(ref 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. BeginInvoke(InvalidateArrange); } } /// /// Determines whether the plot is currently visible to the user. /// /// true if the plot is currently visible to the user; otherwise, false. protected bool IsVisibleToUser() { return IsUserVisible(this); } /// /// Determines whether the specified element is currently visible to the user. /// /// The element. /// true if the specified element is currently visible to the user; otherwise, false. private static bool IsUserVisible(Control element) { return element.IsEffectivelyVisible; } /// /// Called when the size of the control is changed. /// /// The sender. /// The new size private void OnSizeChanged(Rect size) { if (size.Height > 0 && size.Width > 0) { InvalidatePlot(false); } } /// /// Gets the relevant parent. /// /// Type of the relevant parent /// The object. /// The relevant parent. private Control GetRelevantParent(Visual obj) where T : Control { var container = obj.GetVisualParent(); if (container is ContentPresenter contentPresenter) { container = GetRelevantParent(contentPresenter); } if (container is Panel panel) { container = GetRelevantParent(panel); } if (!(container is T) && (container != null)) { container = GetRelevantParent(container); } return (Control)container; } /// /// Updates the visuals. /// private void UpdateVisuals() { if (canvas == null || renderContext == null) { return; } if (!IsVisibleToUser()) { return; } // Clear the canvas canvas.Children.Clear(); if (ActualModel?.Background.IsVisible() == true) { canvas.Background = ActualModel.Background.ToBrush(); } else { canvas.Background = null; } if (ActualModel != null) { lock (this.ActualModel.SyncRoot) { var updateState = (Interlocked.Exchange(ref isUpdateRequired, 0)); if (updateState > 0) { ((IPlotModel)ActualModel).Update(updateState == 2); } if (DisconnectCanvasWhileUpdating) { // TODO: profile... not sure if this makes any difference var idx = panel.Children.IndexOf(canvas); if (idx != -1) { panel.Children.RemoveAt(idx); } ((IPlotModel)ActualModel).Render(renderContext, new OxyRect(0, 0, canvas.Bounds.Width, canvas.Bounds.Height)); // reinsert the canvas again if (idx != -1) { panel.Children.Insert(idx, canvas); } } else { ((IPlotModel)ActualModel).Render(renderContext, new OxyRect(0, 0, canvas.Bounds.Width, canvas.Bounds.Height)); } } } } /// /// Invokes the specified action on the dispatcher, if necessary. /// /// The action. private static void BeginInvoke(Action action) { if (Dispatcher.UIThread.CheckAccess()) { action?.Invoke(); } else { Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Loaded); } } } }