// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) 2014 OxyPlot contributors // // // The tracker control. // // -------------------------------------------------------------------------------------------------------------------- using Avalonia; namespace OxyPlot.Avalonia { using global::Avalonia.Controls; using global::Avalonia.Controls.Presenters; using global::Avalonia.Controls.Primitives; using global::Avalonia.Controls.Shapes; using global::Avalonia.Layout; using global::Avalonia.Media; using global::Avalonia.VisualTree; using System.Collections.Generic; using System.Linq; /// /// The tracker control. /// public class TrackerControl : ContentControl { /// /// Identifies the dependency property. /// public static readonly StyledProperty HorizontalLineVisibilityProperty = AvaloniaProperty.Register(nameof(HorizontalLineVisibility), true); /// /// Identifies the dependency property. /// public static readonly StyledProperty VerticalLineVisibilityProperty = AvaloniaProperty.Register(nameof(VerticalLineVisibility), true); /// /// Identifies the dependency property. /// public static readonly StyledProperty LineStrokeProperty = AvaloniaProperty.Register(nameof(LineStroke)); /// /// Identifies the dependency property. /// public static readonly StyledProperty LineExtentsProperty = AvaloniaProperty.Register(nameof(LineExtents), new OxyRect()); /// /// Identifies the dependency property. /// public static readonly StyledProperty> LineDashArrayProperty = AvaloniaProperty.Register>(nameof(LineDashArray)); /// /// Identifies the dependency property. /// public static readonly StyledProperty ShowPointerProperty = AvaloniaProperty.Register(nameof(ShowPointer), true); /// /// Identifies the dependency property. /// public static new readonly StyledProperty CornerRadiusProperty = AvaloniaProperty.Register(nameof(CornerRadius), 0.0); /// /// Identifies the dependency property. /// public static readonly StyledProperty DistanceProperty = AvaloniaProperty.Register(nameof(Distance), 7.0); /// /// Identifies the dependency property. /// public static readonly StyledProperty CanCenterHorizontallyProperty = AvaloniaProperty.Register(nameof(CanCenterHorizontally), true); /// /// Identifies the dependency property. /// public static readonly StyledProperty CanCenterVerticallyProperty = AvaloniaProperty.Register(nameof(CanCenterVertically), true); /// /// Identifies the dependency property. /// public static readonly StyledProperty PositionProperty = AvaloniaProperty.Register(nameof(Position), new ScreenPoint()); /// /// The path part string. /// private const string PartPath = "PART_Path"; /// /// The content part string. /// private const string PartContent = "PART_Content"; /// /// The content container part string. /// private const string PartContentContainer = "PART_ContentContainer"; /// /// The horizontal line part string. /// private const string PartHorizontalLine = "PART_HorizontalLine"; /// /// The vertical line part string. /// private const string PartVerticalLine = "PART_VerticalLine"; /// /// The content. /// private ContentPresenter content; /// /// The horizontal line. /// private Line horizontalLine; /// /// The path. /// private Path path; /// /// The content container. /// private Panel contentContainer; /// /// The vertical line. /// private Line verticalLine; /// /// Initializes static members of the class. /// static TrackerControl() { ClipToBoundsProperty.OverrideDefaultValue(false); PositionProperty.Changed.AddClassHandler(PositionChanged); } /// /// Gets or sets HorizontalLineVisibility. /// public bool HorizontalLineVisibility { get { return GetValue(HorizontalLineVisibilityProperty); } set { SetValue(HorizontalLineVisibilityProperty, value); } } /// /// Gets or sets VerticalLineVisibility. /// public bool VerticalLineVisibility { get { return GetValue(VerticalLineVisibilityProperty); } set { SetValue(VerticalLineVisibilityProperty, value); } } /// /// Gets or sets LineStroke. /// public IBrush LineStroke { get { return GetValue(LineStrokeProperty); } set { SetValue(LineStrokeProperty, value); } } /// /// Gets or sets LineExtents. /// public OxyRect LineExtents { get { return GetValue(LineExtentsProperty); } set { SetValue(LineExtentsProperty, value); } } /// /// Gets or sets LineDashArray. /// public List LineDashArray { get { return GetValue(LineDashArrayProperty); } set { SetValue(LineDashArrayProperty, value); } } /// /// Gets or sets a value indicating whether to show a 'pointer' on the border. /// public bool ShowPointer { get { return GetValue(ShowPointerProperty); } set { SetValue(ShowPointerProperty, value); } } /// /// Gets or sets the corner radius (only used when ShowPoint=false). /// public new double CornerRadius { get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } } /// /// Gets or sets the distance of the content container from the trackers Position. /// public double Distance { get { return GetValue(DistanceProperty); } set { SetValue(DistanceProperty, value); } } /// /// Gets or sets a value indicating whether the tracker can center its content box horizontally. /// public bool CanCenterHorizontally { get { return GetValue(CanCenterHorizontallyProperty); } set { SetValue(CanCenterHorizontallyProperty, value); } } /// /// Gets or sets a value indicating whether the tracker can center its content box vertically. /// public bool CanCenterVertically { get { return GetValue(CanCenterVerticallyProperty); } set { SetValue(CanCenterVerticallyProperty, value); } } /// /// Gets or sets Position of the tracker. /// public ScreenPoint Position { get { return GetValue(PositionProperty); } set { SetValue(PositionProperty, value); } } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); path = e.NameScope.Get(PartPath); content = e.NameScope.Get(PartContent); contentContainer = e.NameScope.Get(PartContentContainer); horizontalLine = e.NameScope.Find(PartHorizontalLine); verticalLine = e.NameScope.Find(PartVerticalLine); UpdatePositionAndBorder(); } /// /// Called when the position is changed. /// /// The sender. /// The instance containing the event data. private static void PositionChanged(AvaloniaObject sender, AvaloniaPropertyChangedEventArgs e) { ((TrackerControl)sender).OnPositionChanged(e); } /// /// Called when the position is changed. /// /// The dependency property changed event args. // ReSharper disable once UnusedParameter.Local private void OnPositionChanged(AvaloniaPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { UpdatePositionAndBorder(); } /// /// Update the position and border of the tracker. /// private void UpdatePositionAndBorder() { if (contentContainer == null) { return; } var pos = this.Position; var lineExtents = this.LineExtents; Canvas.SetLeft(this, pos.X); Canvas.SetTop(this, pos.Y); Control parent = this; while (!(parent is Canvas) && parent != null) { parent = parent.GetVisualParent() as Control; } if (parent == null) { return; } // throw new InvalidOperationException("The TrackerControl must have a Canvas parent."); var canvasWidth = parent.Bounds.Width; var canvasHeight = parent.Bounds.Height; content.Measure(new Size(canvasWidth, canvasHeight)); content.Arrange(new Rect(0, 0, content.DesiredSize.Width, content.DesiredSize.Height)); var contentWidth = content.DesiredSize.Width; var contentHeight = content.DesiredSize.Height; // Minimum allowed margins around the tracker const double MarginLimit = 10; var ha = HorizontalAlignment.Center; if (CanCenterHorizontally) { if (pos.X - (contentWidth / 2) < MarginLimit) { ha = HorizontalAlignment.Left; } if (pos.X + (contentWidth / 2) > canvasWidth - MarginLimit) { ha = HorizontalAlignment.Right; } } else { ha = pos.X < canvasWidth / 2 ? HorizontalAlignment.Left : HorizontalAlignment.Right; } var va = VerticalAlignment.Center; if (CanCenterVertically) { if (pos.Y - (contentHeight / 2) < MarginLimit) { va = VerticalAlignment.Top; } if (ha == HorizontalAlignment.Center) { va = VerticalAlignment.Bottom; if (pos.Y - contentHeight < MarginLimit) { va = VerticalAlignment.Top; } } if (va == VerticalAlignment.Center && pos.Y + (contentHeight / 2) > canvasHeight - MarginLimit) { va = VerticalAlignment.Bottom; } if (va == VerticalAlignment.Top && pos.Y + contentHeight > canvasHeight - MarginLimit) { va = VerticalAlignment.Bottom; } } else { va = pos.Y < canvasHeight / 2 ? VerticalAlignment.Top : VerticalAlignment.Bottom; } var dx = ha == HorizontalAlignment.Center ? -0.5 : ha == HorizontalAlignment.Left ? 0 : -1; var dy = va == VerticalAlignment.Center ? -0.5 : va == VerticalAlignment.Top ? 0 : -1; path.Data = ShowPointer ? CreatePointerBorderGeometry(ha, va, contentWidth, contentHeight, out var margin) : CreateBorderGeometry(ha, va, contentWidth, contentHeight, out margin); content.Margin = margin; contentContainer.Measure(new Size(canvasWidth, canvasHeight)); var contentSize = contentContainer.DesiredSize; contentContainer.RenderTransform = new TranslateTransform { X = dx * contentSize.Width, Y = dy * contentSize.Height }; if (horizontalLine != null) { horizontalLine.StartPoint = new Point(lineExtents.Left - pos.X, 0); horizontalLine.EndPoint = new Point(lineExtents.Right - pos.X, 0); } if (verticalLine != null) { verticalLine.StartPoint = new Point(0, lineExtents.Top - pos.Y); verticalLine.EndPoint = new Point(0, lineExtents.Bottom - pos.Y); } } /// /// Create the border geometry. /// /// The horizontal alignment. /// The vertical alignment. /// The width. /// The height. /// The margin. /// The border geometry. private Geometry CreateBorderGeometry( HorizontalAlignment ha, VerticalAlignment va, double width, double height, out Thickness margin) { var m = Distance; var rect = new Rect( ha == HorizontalAlignment.Left ? m : 0, va == VerticalAlignment.Top ? m : 0, width, height); margin = new Thickness( ha == HorizontalAlignment.Left ? m : 0, va == VerticalAlignment.Top ? m : 0, ha == HorizontalAlignment.Right ? m : 0, va == VerticalAlignment.Bottom ? m : 0); return new RectangleGeometry(rect)/* { RadiusX = this.CornerRadius, RadiusY = this.CornerRadius }*/; } /// /// Create a border geometry with a 'pointer'. /// /// The horizontal alignment. /// The vertical alignment. /// The width. /// The height. /// The margin. /// The border geometry. private Geometry CreatePointerBorderGeometry( HorizontalAlignment ha, VerticalAlignment va, double width, double height, out Thickness margin) { Point[] points = null; var m = Distance; margin = new Thickness(); if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Bottom) { var x1 = width; var x2 = x1 / 2; var y1 = height; margin = new Thickness(0, 0, 0, m); points = new[] { new Point(0, 0), new Point(x1, 0), new Point(x1, y1), new Point(x2 + (m / 2), y1), new Point(x2, y1 + m), new Point(x2 - (m / 2), y1), new Point(0, y1) }; } else if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Top) { var x1 = width; var x2 = x1 / 2; var y0 = m; var y1 = m + height; margin = new Thickness(0, m, 0, 0); points = new[] { new Point(0, y0), new Point(x2 - (m / 2), y0), new Point(x2, 0), new Point(x2 + (m / 2), y0), new Point(x1, y0), new Point(x1, y1), new Point(0, y1) }; } else if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Center) { var x0 = m; var x1 = m + width; var y1 = height; var y2 = y1 / 2; margin = new Thickness(m, 0, 0, 0); points = new[] { new Point(0, y2), new Point(x0, y2 - (m / 2)), new Point(x0, 0), new Point(x1, 0), new Point(x1, y1), new Point(x0, y1), new Point(x0, y2 + (m / 2)) }; } else if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Center) { var x1 = width; var y1 = height; var y2 = y1 / 2; margin = new Thickness(0, 0, m, 0); points = new[] { new Point(x1 + m, y2), new Point(x1, y2 + (m / 2)), new Point(x1, y1), new Point(0, y1), new Point(0, 0), new Point(x1, 0), new Point(x1, y2 - (m / 2)) }; } else if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Top) { m *= 0.67; var x0 = m; var x1 = m + width; var y0 = m; var y1 = m + height; margin = new Thickness(m, m, 0, 0); points = new[] { new Point(0, 0), new Point(m * 2, y0), new Point(x1, y0), new Point(x1, y1), new Point(x0, y1), new Point(x0, m * 2) }; } else if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Top) { m *= 0.67; var x1 = width; var y0 = m; var y1 = m + height; margin = new Thickness(0, m, m, 0); points = new[] { new Point(x1 + m, 0), new Point(x1, y0 + m), new Point(x1, y1), new Point(0, y1), new Point(0, y0), new Point(x1 - m, y0) }; } else if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Bottom) { m *= 0.67; var x0 = m; var x1 = m + width; var y1 = height; margin = new Thickness(m, 0, 0, m); points = new[] { new Point(0, y1 + m), new Point(x0, y1 - m), new Point(x0, 0), new Point(x1, 0), new Point(x1, y1), new Point(x0 + m, y1) }; } else if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Bottom) { m *= 0.67; var x1 = width; var y1 = height; margin = new Thickness(0, 0, m, m); points = new[] { new Point(x1 + m, y1 + m), new Point(x1 - m, y1), new Point(0, y1), new Point(0, 0), new Point(x1, 0), new Point(x1, y1 - m) }; } if (points == null) { return null; } var pc = new List(points.Length); foreach (var p in points) { pc.Add(p); } var segments = new PathSegments(); segments.AddRange(pc.Select(p => new LineSegment { Point = p })); var pf = new PathFigure { StartPoint = points[0], Segments = segments, IsClosed = true }; return new PathGeometry { Figures = new PathFigures { pf } }; } } }