// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; using System.Windows.Shapes; namespace System.Windows.Controls.DataVisualization { /// /// A panel that plots elements on a one dimensional plane. In order to /// minimize collisions it moves elements further and further from the edge /// of the plane based on their priority. Elements that have the same /// priority level are always the same distance from the edge. /// internal class OrientedPanel : Panel { #region public double ActualMinimumDistanceBetweenChildren /// /// Gets the actual minimum distance between children. /// public double ActualMinimumDistanceBetweenChildren { get { return (double)GetValue(ActualMinimumDistanceBetweenChildrenProperty); } private set { SetValue(ActualMinimumDistanceBetweenChildrenProperty, value); } } /// /// Identifies the ActualMinimumDistanceBetweenChildren dependency property. /// public static readonly DependencyProperty ActualMinimumDistanceBetweenChildrenProperty = DependencyProperty.Register( "ActualMinimumDistanceBetweenChildren", typeof(double), typeof(OrientedPanel), new PropertyMetadata(0.0)); #endregion public double ActualMinimumDistanceBetweenChildren #region public double MinimumDistanceBetweenChildren /// /// Gets or sets the minimum distance between children. /// public double MinimumDistanceBetweenChildren { get { return (double)GetValue(MinimumDistanceBetweenChildrenProperty); } set { SetValue(MinimumDistanceBetweenChildrenProperty, value); } } /// /// Identifies the MinimumDistanceBetweenChildren dependency property. /// public static readonly DependencyProperty MinimumDistanceBetweenChildrenProperty = DependencyProperty.Register( "MinimumDistanceBetweenChildren", typeof(double), typeof(OrientedPanel), new PropertyMetadata(0.0, OnMinimumDistanceBetweenChildrenPropertyChanged)); /// /// MinimumDistanceBetweenChildrenProperty property changed handler. /// /// OrientedPanel that changed its MinimumDistanceBetweenChildren. /// Event arguments. private static void OnMinimumDistanceBetweenChildrenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { OrientedPanel source = (OrientedPanel)d; double oldValue = (double)e.OldValue; double newValue = (double)e.NewValue; source.OnMinimumDistanceBetweenChildrenPropertyChanged(oldValue, newValue); } /// /// MinimumDistanceBetweenChildrenProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnMinimumDistanceBetweenChildrenPropertyChanged(double oldValue, double newValue) { InvalidateMeasure(); } #endregion public double MinimumDistanceBetweenChildren #region public double ActualLength /// /// Gets the actual length of the panel. /// public double ActualLength { get { return (double)GetValue(ActualLengthProperty); } } /// /// Identifies the ActualLength dependency property. /// public static readonly DependencyProperty ActualLengthProperty = DependencyProperty.Register( "ActualLength", typeof(double), typeof(OrientedPanel), new PropertyMetadata(0.0)); #endregion public double ActualLength #region public attached double CenterCoordinate /// /// Gets the value of the CenterCoordinate attached property for a specified UIElement. /// /// The UIElement from which the property value is read. /// The CenterCoordinate property value for the UIElement. [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")] public static double GetCenterCoordinate(UIElement element) { if (element == null) { throw new ArgumentNullException("element"); } return (double) element.GetValue(CenterCoordinateProperty); } /// /// Sets the value of the CenterCoordinate attached property to a specified UIElement. /// /// The UIElement to which the attached property is written. /// The needed CenterCoordinate value. [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")] public static void SetCenterCoordinate(UIElement element, double value) { if (element == null) { throw new ArgumentNullException("element"); } element.SetValue(CenterCoordinateProperty, value); } /// /// Identifies the CenterCoordinate dependency property. /// public static readonly DependencyProperty CenterCoordinateProperty = DependencyProperty.RegisterAttached( "CenterCoordinate", typeof(double), typeof(OrientedPanel), new PropertyMetadata(OnCenterCoordinatePropertyChanged)); /// /// CenterCoordinateProperty property changed handler. /// /// UIElement that changed its CenterCoordinate. /// Event arguments. public static void OnCenterCoordinatePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs) { UIElement source = dependencyObject as UIElement; if (source == null) { throw new ArgumentNullException("dependencyObject"); } OrientedPanel parent = VisualTreeHelper.GetParent(source) as OrientedPanel; if (parent != null) { parent.InvalidateMeasure(); } } #endregion public attached double CenterCoordinate #region public double OffsetPadding /// /// Gets or sets the amount of offset padding to add between items. /// public double OffsetPadding { get { return (double)GetValue(OffsetPaddingProperty); } set { SetValue(OffsetPaddingProperty, value); } } /// /// Identifies the OffsetPadding dependency property. /// public static readonly DependencyProperty OffsetPaddingProperty = DependencyProperty.Register( "OffsetPadding", typeof(double), typeof(OrientedPanel), new PropertyMetadata(0.0, OnOffsetPaddingPropertyChanged)); /// /// OffsetPaddingProperty property changed handler. /// /// OrientedPanel that changed its OffsetPadding. /// Event arguments. private static void OnOffsetPaddingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { OrientedPanel source = (OrientedPanel)d; double oldValue = (double)e.OldValue; double newValue = (double)e.NewValue; source.OnOffsetPaddingPropertyChanged(oldValue, newValue); } /// /// OffsetPaddingProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnOffsetPaddingPropertyChanged(double oldValue, double newValue) { this.InvalidateMeasure(); } #endregion public double OffsetPadding #region public attached int Priority /// /// Gets the value of the Priority attached property for a specified UIElement. /// /// The UIElement from which the property value is read. /// The Priority property value for the UIElement. [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")] public static int GetPriority(UIElement element) { if (element == null) { throw new ArgumentNullException("element"); } return (int) element.GetValue(PriorityProperty); } /// /// Sets the value of the Priority attached property to a specified UIElement. /// /// The UIElement to which the attached property is written. /// The needed Priority value. [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")] public static void SetPriority(UIElement element, int value) { if (element == null) { throw new ArgumentNullException("element"); } element.SetValue(PriorityProperty, value); } /// /// Identifies the Priority dependency property. /// public static readonly DependencyProperty PriorityProperty = DependencyProperty.RegisterAttached( "Priority", typeof(int), typeof(OrientedPanel), new PropertyMetadata(OnPriorityPropertyChanged)); /// /// PriorityProperty property changed handler. /// /// UIElement that changed its Priority. /// Event arguments. public static void OnPriorityPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs) { UIElement source = dependencyObject as UIElement; if (source == null) { throw new ArgumentNullException("dependencyObject"); } OrientedPanel parent = VisualTreeHelper.GetParent(source) as OrientedPanel; if (parent != null) { parent.InvalidateMeasure(); } } #endregion public attached int Priority #region public bool IsInverted /// /// Gets or sets a value indicating whether the panel is inverted. /// public bool IsInverted { get { return (bool)GetValue(IsInvertedProperty); } set { SetValue(IsInvertedProperty, value); } } /// /// Identifies the IsInverted dependency property. /// public static readonly DependencyProperty IsInvertedProperty = DependencyProperty.Register( "IsInverted", typeof(bool), typeof(OrientedPanel), new PropertyMetadata(false, OnIsInvertedPropertyChanged)); /// /// IsInvertedProperty property changed handler. /// /// OrientedPanel that changed its IsInverted. /// Event arguments. private static void OnIsInvertedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { OrientedPanel source = (OrientedPanel)d; bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; source.OnIsInvertedPropertyChanged(oldValue, newValue); } /// /// IsInvertedProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnIsInvertedPropertyChanged(bool oldValue, bool newValue) { InvalidateMeasure(); } #endregion public bool IsInverted #region public bool IsReversed /// /// Gets or sets a value indicating whether the direction is reversed. /// public bool IsReversed { get { return (bool)GetValue(IsReversedProperty); } set { SetValue(IsReversedProperty, value); } } /// /// Identifies the IsReversed dependency property. /// public static readonly DependencyProperty IsReversedProperty = DependencyProperty.Register( "IsReversed", typeof(bool), typeof(OrientedPanel), new PropertyMetadata(false, OnIsReversedPropertyChanged)); /// /// IsReversedProperty property changed handler. /// /// OrientedPanel that changed its IsReversed. /// Event arguments. private static void OnIsReversedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { OrientedPanel source = (OrientedPanel)d; bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; source.OnIsReversedPropertyChanged(oldValue, newValue); } /// /// IsReversedProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnIsReversedPropertyChanged(bool oldValue, bool newValue) { InvalidateMeasure(); } #endregion public bool IsReversed #region public Orientation Orientation /// /// Gets or sets the orientation of the panel. /// public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } /// /// Identifies the Orientation dependency property. /// public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( "Orientation", typeof(Orientation), typeof(OrientedPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationPropertyChanged)); /// /// OrientationProperty property changed handler. /// /// OrientedPanel that changed its Orientation. /// Event arguments. private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { OrientedPanel source = (OrientedPanel)d; Orientation newValue = (Orientation)e.NewValue; source.OnOrientationPropertyChanged(newValue); } /// /// OrientationProperty property changed handler. /// /// New value. protected virtual void OnOrientationPropertyChanged(Orientation newValue) { UpdateActualLength(); InvalidateMeasure(); } #endregion public Orientation Orientation /// /// Gets or sets the offset of the edge to use for each priority group. /// private IDictionary PriorityOffsets { get; set; } /// /// Instantiates a new instance of the OrientedPanel class. /// public OrientedPanel() { UpdateActualLength(); } /// /// Updates the actual length property. /// private void UpdateActualLength() { this.SetBinding(ActualLengthProperty, new Binding((Orientation == Orientation.Horizontal) ? "ActualWidth" : "ActualHeight") { Source = this }); } /// /// Returns a sequence of ranges for a given sequence of children and a /// length selector. /// /// A sequence of children. /// A function that returns a length given /// a UIElement. /// A sequence of ranges. private static IEnumerable> GetRanges(IEnumerable children, Func lengthSelector) { return children .Select(child => { double centerCoordinate = GetCenterCoordinate(child); double halfLength = lengthSelector(child) / 2; return new Range(centerCoordinate - halfLength, centerCoordinate + halfLength); }); } /// /// Measures children and determines necessary size. /// /// The available size. /// The necessary size. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Linq use artificially increases cyclomatic complexity. Linq functions are well-understood.")] protected override Size MeasureOverride(Size availableSize) { double offset = 0.0; if (Children.Count > 0) { Size totalSize = new Size(double.PositiveInfinity, double.PositiveInfinity); foreach (UIElement child in this.Children) { child.Measure(totalSize); } Func lengthSelector = null; Func offsetSelector = null; if (Orientation == Orientation.Horizontal) { lengthSelector = child => GetCorrectedDesiredSize(child).Width; offsetSelector = child => GetCorrectedDesiredSize(child).Height; } else { lengthSelector = child => GetCorrectedDesiredSize(child).Height; offsetSelector = child => GetCorrectedDesiredSize(child).Width; } IEnumerable> priorityGroups = from child in Children.CastWrapper() group child by GetPriority(child) into priorityGroup select priorityGroup; ActualMinimumDistanceBetweenChildren = (from priorityGroup in priorityGroups let orderedElements = (from element in priorityGroup orderby GetCenterCoordinate(element) ascending select element).ToList() where orderedElements.Count >= 2 select (EnumerableFunctions.Zip( orderedElements, orderedElements.Skip(1), (leftElement, rightElement) => { double halfLeftLength = lengthSelector(leftElement) / 2; double leftCenterCoordinate = GetCenterCoordinate(leftElement); double halfRightLength = lengthSelector(rightElement) / 2; double rightCenterCoordinate = GetCenterCoordinate(rightElement); return (rightCenterCoordinate - halfRightLength) - (leftCenterCoordinate + halfLeftLength); })) .Min()) .MinOrNullable() ?? MinimumDistanceBetweenChildren; IEnumerable priorities = Children .CastWrapper() .Select(child => GetPriority(child)).Distinct().OrderBy(priority => priority).ToList(); PriorityOffsets = new Dictionary(); foreach (int priority in priorities) { PriorityOffsets[priority] = 0.0; } IEnumerable> priorityPairs = EnumerableFunctions.Zip(priorities, priorities.Skip(1), (previous, next) => new Tuple(previous, next)); foreach (Tuple priorityPair in priorityPairs) { IEnumerable currentPriorityChildren = Children.CastWrapper().Where(child => GetPriority(child) == priorityPair.Item1).ToList(); IEnumerable> currentPriorityRanges = GetRanges(currentPriorityChildren, lengthSelector); IEnumerable nextPriorityChildren = Children.CastWrapper().Where(child => GetPriority(child) == priorityPair.Item2).ToList(); IEnumerable> nextPriorityRanges = GetRanges(nextPriorityChildren, lengthSelector); bool intersects = (from currentPriorityRange in currentPriorityRanges from nextPriorityRange in nextPriorityRanges select currentPriorityRange.IntersectsWith(nextPriorityRange)) .Any(value => value); if (intersects) { double maxCurrentPriorityChildOffset = currentPriorityChildren .Select(child => offsetSelector(child)) .MaxOrNullable() ?? 0.0; offset += maxCurrentPriorityChildOffset + OffsetPadding; } PriorityOffsets[priorityPair.Item2] = offset; } offset = (Children .CastWrapper() .GroupBy(child => GetPriority(child)) .Select( group => group .Select(child => PriorityOffsets[group.Key] + offsetSelector(child)) .MaxOrNullable())) .Where(num => num.HasValue) .Select(num => num.Value) .MaxOrNullable() ?? 0.0; } if (Orientation == Orientation.Horizontal) { return new Size(0, offset); } else { return new Size(offset, 0); } } /// /// Arranges items according to position and priority. /// /// The final size of the panel. /// The final size of the control. protected override Size ArrangeOverride(Size finalSize) { foreach (UIElement child in Children) { double x = 0.0; double y = 0.0; x = GetCenterCoordinate(child); y = PriorityOffsets[GetPriority(child)]; double totalLength = 0.0; double totalOffsetLength = 0.0; double length = 0.0; double offsetLength = 0.0; Size childCorrectedDesiredSize = GetCorrectedDesiredSize(child); if (Orientation == Orientation.Horizontal) { totalLength = finalSize.Width; length = childCorrectedDesiredSize.Width; offsetLength = childCorrectedDesiredSize.Height; totalOffsetLength = finalSize.Height; } else if (Orientation == Orientation.Vertical) { totalLength = finalSize.Height; length = childCorrectedDesiredSize.Height; offsetLength = childCorrectedDesiredSize.Width; totalOffsetLength = finalSize.Width; } double halfLength = length / 2; double left = 0.0; double top = 0.0; if (!IsReversed) { left = x - halfLength; } else { left = totalLength - Math.Round(x + halfLength); } if (!IsInverted) { top = y; } else { top = totalOffsetLength - Math.Round(y + offsetLength); } left = Math.Min(Math.Round(left), totalLength - 1); top = Math.Round(top); if (Orientation == Orientation.Horizontal) { child.Arrange(new Rect(left, top, length, offsetLength)); } else if (Orientation == Orientation.Vertical) { child.Arrange(new Rect(top, left, offsetLength, length)); } } return finalSize; } /// /// Gets the "corrected" DesiredSize (for Line instances); one that is /// more consistent with how the elements actually render. /// /// UIElement to get the size for. /// Corrected size. private static Size GetCorrectedDesiredSize(UIElement element) { Line elementAsLine = element as Line; if (null != elementAsLine) { return new Size( Math.Max(elementAsLine.StrokeThickness, elementAsLine.X2 - elementAsLine.X1), Math.Max(elementAsLine.StrokeThickness, elementAsLine.Y2 - elementAsLine.Y1)); } else { return element.DesiredSize; } } } }