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