// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) 2020 OxyPlot contributors
//
//
// The tracker control.
//
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.Wpf
{
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
///
/// The tracker control.
///
public class TrackerControl : ContentControl
{
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty HorizontalLineVisibilityProperty =
DependencyProperty.Register(
nameof(HorizontalLineVisibility),
typeof(Visibility),
typeof(TrackerControl),
new PropertyMetadata(Visibility.Visible));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty VerticalLineVisibilityProperty =
DependencyProperty.Register(
nameof(VerticalLineVisibility),
typeof(Visibility),
typeof(TrackerControl),
new PropertyMetadata(Visibility.Visible));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty LineThicknessProperty = DependencyProperty.Register(
nameof(LineThickness), typeof(double), typeof(TrackerControl), new PropertyMetadata(1.0));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty LineStrokeProperty = DependencyProperty.Register(
nameof(LineStroke), typeof(Brush), typeof(TrackerControl), new PropertyMetadata(null));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty LineExtentsProperty = DependencyProperty.Register(
nameof(LineExtents), typeof(OxyRect), typeof(TrackerControl), new PropertyMetadata(new OxyRect()));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty LineDashArrayProperty = DependencyProperty.Register(
nameof(LineDashArray), typeof(DoubleCollection), typeof(TrackerControl), new PropertyMetadata(null));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty BorderEdgeModeProperty = DependencyProperty.Register(
nameof(BorderEdgeMode), typeof(EdgeMode), typeof(TrackerControl));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty ShowPointerProperty = DependencyProperty.Register(
nameof(ShowPointer), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(
nameof(CornerRadius), typeof(double), typeof(TrackerControl), new PropertyMetadata(0.0));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty DistanceProperty = DependencyProperty.Register(
nameof(Distance), typeof(double), typeof(TrackerControl), new PropertyMetadata(7.0));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty CanCenterHorizontallyProperty =
DependencyProperty.Register(
nameof(CanCenterHorizontally), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty CanCenterVerticallyProperty =
DependencyProperty.Register(
nameof(CanCenterVertically), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty PositionProperty = DependencyProperty.Register(
nameof(Position),
typeof(ScreenPoint),
typeof(TrackerControl),
new PropertyMetadata(new ScreenPoint(), PositionChanged));
///
/// 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 Grid contentContainer;
///
/// The vertical line.
///
private Line verticalLine;
///
/// Initializes static members of the class.
///
static TrackerControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TrackerControl), new FrameworkPropertyMetadata(typeof(TrackerControl)));
}
///
/// Gets or sets BorderEdgeMode.
///
public EdgeMode BorderEdgeMode
{
get => (EdgeMode)this.GetValue(BorderEdgeModeProperty);
set => this.SetValue(BorderEdgeModeProperty, value);
}
///
/// Gets or sets HorizontalLineVisibility.
///
public Visibility HorizontalLineVisibility
{
get => (Visibility)this.GetValue(HorizontalLineVisibilityProperty);
set => this.SetValue(HorizontalLineVisibilityProperty, value);
}
///
/// Gets or sets VerticalLineVisibility.
///
public Visibility VerticalLineVisibility
{
get => (Visibility)this.GetValue(VerticalLineVisibilityProperty);
set => this.SetValue(VerticalLineVisibilityProperty, value);
}
///
/// Gets or sets LineThickness.
///
public double LineThickness
{
get => (double)this.GetValue(LineThicknessProperty);
set => this.SetValue(LineThicknessProperty, value);
}
///
/// Gets or sets LineStroke.
///
public Brush LineStroke
{
get => (Brush)this.GetValue(LineStrokeProperty);
set => this.SetValue(LineStrokeProperty, value);
}
///
/// Gets or sets LineExtents.
///
public OxyRect LineExtents
{
get => (OxyRect)this.GetValue(LineExtentsProperty);
set => this.SetValue(LineExtentsProperty, value);
}
///
/// Gets or sets LineDashArray.
///
public DoubleCollection LineDashArray
{
get => (DoubleCollection)this.GetValue(LineDashArrayProperty);
set => this.SetValue(LineDashArrayProperty, value);
}
///
/// Gets or sets a value indicating whether to show a 'pointer' on the border.
///
public bool ShowPointer
{
get => (bool)this.GetValue(ShowPointerProperty);
set => this.SetValue(ShowPointerProperty, value);
}
///
/// Gets or sets the corner radius (only used when ShowPoint=false).
///
public double CornerRadius
{
get => (double)this.GetValue(CornerRadiusProperty);
set => this.SetValue(CornerRadiusProperty, value);
}
///
/// Gets or sets the distance of the content container from the trackers Position.
///
public double Distance
{
get => (double)this.GetValue(DistanceProperty);
set => this.SetValue(DistanceProperty, value);
}
///
/// Gets or sets a value indicating whether the tracker can center its content box horizontally.
///
public bool CanCenterHorizontally
{
get => (bool)this.GetValue(CanCenterHorizontallyProperty);
set => this.SetValue(CanCenterHorizontallyProperty, value);
}
///
/// Gets or sets a value indicating whether the tracker can center its content box vertically.
///
public bool CanCenterVertically
{
get => (bool)this.GetValue(CanCenterVerticallyProperty);
set => this.SetValue(CanCenterVerticallyProperty, value);
}
///
/// Gets or sets Position of the tracker.
///
public ScreenPoint Position
{
get => (ScreenPoint)this.GetValue(PositionProperty);
set => this.SetValue(PositionProperty, value);
}
///
/// When overridden in a derived class, is invoked whenever application code or internal processes call .
///
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.path = this.GetTemplateChild(PartPath) as Path;
this.content = this.GetTemplateChild(PartContent) as ContentPresenter;
this.contentContainer = this.GetTemplateChild(PartContentcontainer) as Grid;
this.horizontalLine = this.GetTemplateChild(PartHorizontalline) as Line;
this.verticalLine = this.GetTemplateChild(PartVerticalline) as Line;
if (this.contentContainer == null)
{
throw new InvalidOperationException($"The TrackerControl template must contain a content container with name +'{PartContentcontainer}'");
}
if (this.path == null)
{
throw new InvalidOperationException($"The TrackerControl template must contain a Path with name +'{PartPath}'");
}
if (this.content == null)
{
throw new InvalidOperationException($"The TrackerControl template must contain a ContentPresenter with name +'{PartContent}'");
}
this.UpdatePositionAndBorder();
}
///
/// Called when the position is changed.
///
/// The sender.
/// The instance containing the event data.
private static void PositionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs 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(DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
this.UpdatePositionAndBorder();
}
///
/// Update the position and border of the tracker.
///
private void UpdatePositionAndBorder()
{
if (this.contentContainer == null)
{
return;
}
Canvas.SetLeft(this.contentContainer, this.Position.X);
Canvas.SetTop(this.contentContainer, this.Position.Y);
FrameworkElement parent = this;
while (!(parent is Canvas) && parent != null)
{
parent = VisualTreeHelper.GetParent(parent) as FrameworkElement;
}
if (parent == null)
{
return;
}
// throw new InvalidOperationException("The TrackerControl must have a Canvas parent.");
double canvasWidth = parent.ActualWidth;
double canvasHeight = parent.ActualHeight;
this.content.Measure(new Size(canvasWidth, canvasHeight));
this.content.Arrange(new Rect(0, 0, this.content.DesiredSize.Width, this.content.DesiredSize.Height));
double contentWidth = this.content.DesiredSize.Width;
double contentHeight = this.content.DesiredSize.Height;
// Minimum allowed margins around the tracker
const double MarginLimit = 10;
var ha = HorizontalAlignment.Center;
if (this.CanCenterHorizontally)
{
if (this.Position.X - (contentWidth / 2) < MarginLimit)
{
ha = HorizontalAlignment.Left;
}
if (this.Position.X + (contentWidth / 2) > canvasWidth - MarginLimit)
{
ha = HorizontalAlignment.Right;
}
}
else
{
ha = this.Position.X < canvasWidth / 2 ? HorizontalAlignment.Left : HorizontalAlignment.Right;
}
var va = VerticalAlignment.Center;
if (this.CanCenterVertically)
{
if (this.Position.Y - (contentHeight / 2) < MarginLimit)
{
va = VerticalAlignment.Top;
}
if (ha == HorizontalAlignment.Center)
{
va = VerticalAlignment.Bottom;
if (this.Position.Y - contentHeight < MarginLimit)
{
va = VerticalAlignment.Top;
}
}
if (va == VerticalAlignment.Center && this.Position.Y + (contentHeight / 2) > canvasHeight - MarginLimit)
{
va = VerticalAlignment.Bottom;
}
if (va == VerticalAlignment.Top && this.Position.Y + contentHeight > canvasHeight - MarginLimit)
{
va = VerticalAlignment.Bottom;
}
}
else
{
va = this.Position.Y < canvasHeight / 2 ? VerticalAlignment.Top : VerticalAlignment.Bottom;
}
double dx = ha == HorizontalAlignment.Center ? -0.5 : ha == HorizontalAlignment.Left ? 0 : -1;
double dy = va == VerticalAlignment.Center ? -0.5 : va == VerticalAlignment.Top ? 0 : -1;
this.path.Data = this.ShowPointer
? this.CreatePointerBorderGeometry(ha, va, contentWidth, contentHeight, out Thickness margin)
: this.CreateBorderGeometry(ha, va, contentWidth, contentHeight, out margin);
this.content.Margin = margin;
this.contentContainer.Measure(new Size(canvasWidth, canvasHeight));
var contentSize = this.contentContainer.DesiredSize;
this.contentContainer.RenderTransform = new TranslateTransform
{
X = dx * contentSize.Width,
Y = dy * contentSize.Height
};
var pos = this.Position;
if (this.horizontalLine != null)
{
if (this.LineExtents.Width > 0)
{
this.horizontalLine.X1 = this.LineExtents.Left;
this.horizontalLine.X2 = this.LineExtents.Right;
}
else
{
this.horizontalLine.X1 = 0;
this.horizontalLine.X2 = canvasWidth;
}
this.horizontalLine.Y1 = pos.Y;
this.horizontalLine.Y2 = pos.Y;
}
if (this.verticalLine != null)
{
if (this.LineExtents.Width > 0)
{
this.verticalLine.Y1 = this.LineExtents.Top;
this.verticalLine.Y2 = this.LineExtents.Bottom;
}
else
{
this.verticalLine.Y1 = 0;
this.verticalLine.Y2 = canvasHeight;
}
this.verticalLine.X1 = pos.X;
this.verticalLine.X2 = pos.X;
}
}
///
/// 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)
{
double m = this.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 = 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;
double m = this.Distance;
margin = new Thickness();
if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Bottom)
{
double x0 = 0;
double x1 = width;
double x2 = (x0 + x1) / 2;
double y0 = 0;
double y1 = height;
margin = new Thickness(0, 0, 0, m);
points = new[]
{
new Point(x0, y0), new Point(x1, y0), new Point(x1, y1), new Point(x2 + (m / 2), y1),
new Point(x2, y1 + m), new Point(x2 - (m / 2), y1), new Point(x0, y1)
};
}
if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Top)
{
double x0 = 0;
double x1 = width;
double x2 = (x0 + x1) / 2;
double y0 = m;
double y1 = m + height;
margin = new Thickness(0, m, 0, 0);
points = new[]
{
new Point(x0, 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(x0, y1)
};
}
if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Center)
{
double x0 = m;
double x1 = m + width;
double y0 = 0;
double y1 = height;
double y2 = (y0 + y1) / 2;
margin = new Thickness(m, 0, 0, 0);
points = new[]
{
new Point(0, y2), new Point(x0, y2 - (m / 2)), new Point(x0, y0), new Point(x1, y0),
new Point(x1, y1), new Point(x0, y1), new Point(x0, y2 + (m / 2))
};
}
if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Center)
{
double x0 = 0;
double x1 = width;
double y0 = 0;
double y1 = height;
double y2 = (y0 + 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(x0, y1),
new Point(x0, y0), new Point(x1, y0), new Point(x1, y2 - (m / 2))
};
}
if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Top)
{
m *= 0.67;
double x0 = m;
double x1 = m + width;
double y0 = m;
double 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)
};
}
if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Top)
{
m *= 0.67;
double x0 = 0;
double x1 = width;
double y0 = m;
double 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(x0, y1),
new Point(x0, y0), new Point(x1 - m, y0)
};
}
if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Bottom)
{
m *= 0.67;
double x0 = m;
double x1 = m + width;
double y0 = 0;
double y1 = height;
margin = new Thickness(m, 0, 0, m);
points = new[]
{
new Point(0, y1 + m), new Point(x0, y1 - m), new Point(x0, y0), new Point(x1, y0),
new Point(x1, y1), new Point(x0 + m, y1)
};
}
if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Bottom)
{
m *= 0.67;
double x0 = 0;
double x1 = width;
double y0 = 0;
double y1 = height;
margin = new Thickness(0, 0, m, m);
points = new[]
{
new Point(x1 + m, y1 + m), new Point(x1 - m, y1), new Point(x0, y1), new Point(x0, y0),
new Point(x1, y0), new Point(x1, y1 - m)
};
}
if (points == null)
{
return null;
}
var pc = new PointCollection(points.Length);
foreach (var p in points)
{
pc.Add(p);
}
var segments = new PathSegmentCollection { new PolyLineSegment { Points = pc } };
var pf = new PathFigure { StartPoint = points[0], Segments = segments, IsClosed = true };
return new PathGeometry { Figures = new PathFigureCollection { pf } };
}
}
}