File: cdf\src\NetFx40\Tools\System.Activities.Presentation\System\Activities\Presentation\FreeFormEditing\ConnectorEditor.cs
Project: ndp\System.Data.csproj (System.Data)
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
 
namespace System.Activities.Presentation.FreeFormEditing
{
    using System;
    using System.Activities.Presentation;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Runtime;
    using System.Diagnostics.CodeAnalysis;
    using System.Windows.Media;
    using System.Activities.Presentation.Internal.PropertyEditing;
 
    internal class ConnectorEditor
    {
        const double EditPointRadius = 4;
        const double EditPointHitTestRadius = 9;
        const int minLengthForSegmentEditPoint = 10;
        EditPoint activeEditPoint;
        AdornerLayer adornerLayer;
        Connector editedConnector;
        List<EditPoint> editPoints;
        FreeFormPanel parentPanel;
 
        public ConnectorEditor(FreeFormPanel panel, Connector connector)
        {
            if (panel == null)
            {
                throw FxTrace.Exception.AsError(new ArgumentNullException("panel"));
            }
            if (connector == null)
            {
                throw FxTrace.Exception.AsError(new ArgumentNullException("connector"));
            }
            this.editPoints = new List<EditPoint>();
            this.parentPanel = panel;
            this.editedConnector = connector;
            this.activeEditPoint = null;
            connector.IsSelected = true;
            // When the ConnectorEditor is active, we allow reconnecting the start point of the Connector instead
            // of creating a new transition that shares the same trigger. So we need to disable tooltips and 
            // highlighting effects for all overlapping start dots.
            this.SetIsHitTestVisibleForOverlappingStartDots(false);
            DisplayEditPoints();
        }
        public bool BeingEdited
        {
            get
            {
                return (this.activeEditPoint != null);
            }
        }
 
        public bool IsConnectorStartBeingMoved
        {
            get
            {
                return (this.BeingEdited && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint
                    && this.editedConnector.Points[0] != this.EditPoints[0].Location);
            }
        }
 
        public bool IsConnectorEndBeingMoved
        {
            get
            {
                return (this.BeingEdited && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint
                    && this.editedConnector.Points[this.editedConnector.Points.Count - 1] != this.EditPoints[this.EditPoints.Count - 1].Location);
            }
        }
 
        public Connector Connector
        {
            get
            {
                return this.editedConnector;
            }
 
            set
            {
                this.editedConnector = value;
            }
        }
 
        List<EditPoint> EditPoints
        {
            get
            {
                return this.editPoints;
            }
        }
 
        public List<Point> ConnectorEditorLocation
        {
            get
            {
                return this.GetPointsFromEditPoints();
            }
        }
 
        void SetIsHitTestVisibleForOverlappingStartDots(bool hitTestVisible)
        {
            ConnectionPoint srcConnectionPoint = FreeFormPanel.GetSourceConnectionPoint(this.Connector);
            foreach (Connector overlappingConnector in srcConnectionPoint.AttachedConnectors)
            {
                if (overlappingConnector.StartDot != null)
                {
                    overlappingConnector.StartDot.IsHitTestVisible = hitTestVisible;
                }
            }
        }
 
        //If the result is true this method also sets the currently active edit point.
        public bool EditPointsHitTest(Point pt)
        {
            if (this.EditPoints.Count > 0)
            {
                foreach (EditPoint editPoint in this.EditPoints)
                {
                    if (DesignerGeometryHelper.DistanceBetweenPoints(pt, editPoint.Location) <= EditPointHitTestRadius)
                    {
                        this.activeEditPoint = editPoint;
                        return true;
                    }
                }
            }
            return false;
        }
 
        //Connector editing is completed. This function saves the state of the connectorEditor into the corresponding connector.
        //Returns whether the Editor was persisted or not. It might not be persisted if Connector end points do not lie on a designer.
        public bool Persist(Point finalSnappedPoint)
        {
            List<Point> segments = new List<Point>();
            this.Update(finalSnappedPoint);
            if (this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
            {
                return false;
            }
            segments = this.GetPointsFromEditPoints();
            this.parentPanel.UpdateConnectorPoints(Connector, segments);
            this.activeEditPoint = null;
            RemoveAdorners();
            DisplayEditPoints();
            return true;
        }
 
        //The Connector editor is to be destroyed. Remove the adorners on the editor. activeEditPoint=null sets BeingEdited property to false.
        public void Remove()
        {
            this.activeEditPoint = null;
            RemoveAdorners();
            this.EditPoints.Clear();
            this.Connector.IsSelected = false;
            // Restore the IsHitTestVisible property
            this.SetIsHitTestVisibleForOverlappingStartDots(true);
            this.Connector = null;
            this.parentPanel = null;
        }
 
        //This method removes the existing adorner on the edited connector, updates the active edit points and creates new adorners.
        public void Update(Point newPoint)
        {
            RemoveAdorners();
            UpdateEditPoints(newPoint);
            Fx.Assert(this.activeEditPoint != null, "activeEditPoint is null");
            adornerLayer.Add(new EditPointAdorner(this, editedConnector, true));
        }
 
        //Add edit points of specified type
        void AddEditPoints(EditPoint.EditPointTypes editPointType)
        {
            if (editPointType == EditPoint.EditPointTypes.ConnectionEditPoint)
            {
                if (this.editPoints.Count == 0 || !this.editPoints[0].Location.Equals(editedConnector.Points[0]))
                {
                    this.editPoints.Insert(0, new EditPoint(EditPoint.EditPointTypes.ConnectionEditPoint, editedConnector.Points[0]));
                }
 
                if (this.editPoints.Count < 2 || !this.editPoints[this.editPoints.Count - 1].Equals(editedConnector.Points[editedConnector.Points.Count - 1]))
                {
                    editPoints.Add(new EditPoint(EditPoint.EditPointTypes.ConnectionEditPoint, editedConnector.Points[editedConnector.Points.Count - 1]));
                }
            }
            else if (editPointType == EditPoint.EditPointTypes.MultiSegmentEditPoint)
            {
                if (this.editPoints.Count == 2)
                {
                    List<Point> segments = new List<Point>(this.editedConnector.Points);
                    if (segments.Count > 0)
                    {
                        segments.RemoveAt(0);
                        segments.RemoveAt(segments.Count - 1);
                    }
 
                    for (int i = 0; i < segments.Count; i++)
                    {
                        this.editPoints.Insert(this.editPoints.Count - 1, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, segments[i]));
                    }
                }
                else
                {
                    Fx.Assert(false, "EditPoints.Count is not 2.");
                }
            }
        }
 
        void CreateEditPoints()
        {
            this.editPoints.Clear();
 
            AddEditPoints(EditPoint.EditPointTypes.ConnectionEditPoint);
            AddEditPoints(EditPoint.EditPointTypes.MultiSegmentEditPoint);
 
            bool validEditPoints = ValidateEditPoints();
            Fx.Assert(validEditPoints, "Validating EditPoints failed.");
        }
 
        void DisplayEditPoints()
        {
            CreateEditPoints();
            adornerLayer = AdornerLayer.GetAdornerLayer(editedConnector);
            if (adornerLayer != null)
            {
                adornerLayer.Add(new EditPointAdorner(this, editedConnector, false));
            }
        }
 
        List<Point> GetPointsFromEditPoints()
        {
            List<Point> segments = new List<Point>();
            //Connection end points will never be moved/removed in following two function calls. Hence passing null as pointsToRetain.
            RemoveEditPointSegmentsWithinTolerance(null);
            RemoveCoincidingEditPoints(null);
            for (int i = 0; i < this.EditPoints.Count; i++)
            {
                segments.Add(this.EditPoints[i].Location);
            }
            return segments;
        }
 
        void RemoveAdorners()
        {
            if (adornerLayer != null && editedConnector != null)
            {
                Adorner[] adorners = adornerLayer.GetAdorners(editedConnector);
                if (adorners != null)
                {
                    foreach (Adorner adorner in adorners)
                    {
                        adornerLayer.Remove(adorner);
                    }
                }
            }
        }
 
        //Remove points with the same slope
        void RemoveCoincidingEditPoints()
        {
            if (this.editPoints.Count < 2 ||
                this.editPoints[0].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
                this.editPoints[this.editPoints.Count - 1].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
                (this.activeEditPoint != null && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint))
            {
                return;
            }
 
            //Create list of points to retain
            List<EditPoint> editPointsToRetain = new List<EditPoint>(this.editPoints.Count);
            for (int i = 0; i < this.editPoints.Count; i++)
            {
                if (this.editPoints[i].Type != EditPoint.EditPointTypes.MultiSegmentEditPoint ||
                    this.editPoints[i] == this.activeEditPoint)
                {
                    editPointsToRetain.Add(this.editPoints[i]);
                }
            }
 
            //Step1: Get rid of all the line segments which are within tolerance range
            RemoveEditPointSegmentsWithinTolerance(editPointsToRetain);
 
            //Step2: We should make sure that the active edit point is always retained but those points which are coincidental are always removed
            RemoveCoincidingEditPoints(editPointsToRetain);
 
            //Step3: Go through each segment and ensure that all the segments are either vertical or horizontal
            for (int i = 0; i < this.editPoints.Count - 1; i++)
            {
                EditPoint current = this.editPoints[i];
                EditPoint next = this.editPoints[i + 1];
 
                double slope = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
                if (slope != 0 && slope != double.MaxValue)
                {
                    Point location = (slope < 1) ? new Point(next.Location.X, current.Location.Y) : new Point(current.Location.X, next.Location.Y);
                    this.editPoints.Insert(i + 1, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, location));
                }
            }
        }
 
        void RemoveEditPointSegmentsWithinTolerance(List<EditPoint> pointsToRetain)
        {
            for (int i = 1; i < this.editPoints.Count - 1; i++)
            {
                EditPoint previous = this.editPoints[i - 1];
                EditPoint current = this.editPoints[i];
                EditPoint next = this.editPoints[i + 1];
 
                if (pointsToRetain == null || !pointsToRetain.Contains(current))
                {
                    double distance = DesignerGeometryHelper.DistanceOfLineSegments(new Point[] { previous.Location, current.Location });
                    if (distance < ConnectorEditor.EditPointRadius && next.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
                    {
                        double slope = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
                        next.Location = (slope < 1) ? new Point(next.Location.X, previous.Location.Y) : new Point(previous.Location.X, next.Location.Y);
                        this.editPoints.Remove(current);
                        i -= 1;
                    }
                    else
                    {
                        distance = DesignerGeometryHelper.DistanceOfLineSegments(new Point[] { current.Location, next.Location });
                        if (distance < ConnectorEditor.EditPointRadius && previous.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
                        {
                            double slope = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, current.Location);
                            previous.Location = (slope < 1) ? new Point(previous.Location.X, next.Location.Y) : new Point(next.Location.X, previous.Location.Y);
                            this.editPoints.Remove(current);
                            i--;
                        }
                    }
                }
            }
 
        }
 
        void RemoveCoincidingEditPoints(List<EditPoint> pointsToRetain)
        {
            for (int i = 1; i < this.EditPoints.Count - 1; i++)
            {
                EditPoint current = this.EditPoints[i];
                if (pointsToRetain == null || !pointsToRetain.Contains(current))
                {
                    EditPoint previous = this.EditPoints[i - 1];
                    EditPoint next = this.EditPoints[i + 1];
                    double slope1 = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, current.Location);
                    double slope2 = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
                    if (Math.Abs(slope1) == Math.Abs(slope2))
                    {
                        this.EditPoints.Remove(current);
                        i -= 1;
                    }
                }
            }
        }
 
        //Remove edit points of specified type
        //This method does not remove this.activeEditPoint.
        void RemoveEditPoints(EditPoint.EditPointTypes editPointType)
        {
            List<EditPoint> editPointsToRemove = new List<EditPoint>();
            for (int i = 0; i < this.editPoints.Count; i++)
            {
                EditPoint editPoint = this.editPoints[i];
                if (editPoint.Type == editPointType)
                {
                    editPointsToRemove.Add(editPoint);
                }
            }
 
            for (int i = 0; i < editPointsToRemove.Count; i++)
            {
                EditPoint editPoint = editPointsToRemove[i];
                if (editPoint != this.activeEditPoint)
                {
                    this.editPoints.Remove(editPoint);
                }
            }
        }
 
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a legacy algorithm.")]
        void UpdateEditPoints(Point newPoint)
        {
            if (this.editPoints.Count < 2 ||
                this.editPoints[0].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
                this.editPoints[this.editPoints.Count - 1].Type != EditPoint.EditPointTypes.ConnectionEditPoint)
            {
                Fx.Assert(false, "EditPoints are invalid");
                return;
            }
 
            if (this.activeEditPoint != null)
            {
                int activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
                EditPoint previous = (activeEditPointIndex > 0) ? this.editPoints[activeEditPointIndex - 1] : null;
                EditPoint next = (activeEditPointIndex < this.editPoints.Count - 1) ? this.editPoints[activeEditPointIndex + 1] : null;
 
                //Note that extra edit points are only added if we are connected to connection point
                if (previous != null && previous.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
                {
                    double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, this.activeEditPoint.Location);
                    Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
 
                    int editPointOffset = Convert.ToInt32(DesignerGeometryHelper.DistanceBetweenPoints(previous.Location, (next != null) ? next.Location : this.activeEditPoint.Location)) / 4;
                    if (orientation == Orientation.Horizontal)
                    {
                        editPointOffset *= (previous.Location.X < this.activeEditPoint.Location.X) ? 1 : -1;
                    }
                    else
                    {
                        editPointOffset *= (previous.Location.Y < this.activeEditPoint.Location.Y) ? 1 : -1;
                    }
 
                    activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
                    Point editPointLocation = (orientation == Orientation.Horizontal) ? new Point(previous.Location.X + editPointOffset, previous.Location.Y) : new Point(previous.Location.X, previous.Location.Y + editPointOffset);
                    previous = new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation);
                    this.editPoints.InsertRange(activeEditPointIndex, new EditPoint[] { new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation), previous });
                }
 
                if (next != null && next.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
                {
                    double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(this.activeEditPoint.Location, next.Location);
                    Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
 
                    int editPointOffset = Convert.ToInt32(DesignerGeometryHelper.DistanceBetweenPoints((previous != null) ? previous.Location : this.activeEditPoint.Location, next.Location)) / 4;
                    if (orientation == Orientation.Horizontal)
                    {
                        editPointOffset *= (this.activeEditPoint.Location.X < next.Location.X) ? -1 : 1;
                    }
                    else
                    {
                        editPointOffset *= (this.activeEditPoint.Location.Y < next.Location.Y) ? -1 : 1;
                    }
 
                    activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
                    Point editPointLocation = (orientation == Orientation.Horizontal) ? new Point(next.Location.X + editPointOffset, next.Location.Y) : new Point(next.Location.X, next.Location.Y + editPointOffset);
                    next = new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation);
                    this.editPoints.InsertRange(activeEditPointIndex + 1, new EditPoint[] { next, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation) });
                }
 
                if (this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
                {
                    Fx.Assert(this.editPoints[0].Type == EditPoint.EditPointTypes.ConnectionEditPoint, "EditPoint type is wrong.");
                    Fx.Assert(this.editPoints[editPoints.Count - 1].Type == EditPoint.EditPointTypes.ConnectionEditPoint, "EditPoint type is wrong.");
                    this.activeEditPoint.Location = newPoint;
 
                    Fx.Assert(this.editPoints.Count > 0, "Some edit point should exist");
                    ConnectionPoint targetConnPt = null;
                    Point[] points = null;
                    Point begin = this.editPoints[0].Location;
                    Point end = this.editPoints[this.editPoints.Count - 1].Location;
 
                    if (typeof(ConnectionPointsAdorner).IsAssignableFrom(Mouse.DirectlyOver.GetType()))
                    {
                        ConnectionPointsAdorner connPtsAdorner = Mouse.DirectlyOver as ConnectionPointsAdorner;
                        targetConnPt = FreeFormPanel.ConnectionPointHitTest(newPoint, connPtsAdorner);
                    }
 
                    if (activeEditPointIndex == 0)
                    {
                        // We are dragging the source point of a connector.
                        ConnectionPoint destConnPt = FreeFormPanel.GetDestinationConnectionPoint(this.editedConnector);
                        if (targetConnPt != null)
                        {
                            points = ConnectorRouter.Route(parentPanel, targetConnPt, destConnPt);
                            this.activeEditPoint.Location = targetConnPt.Location;
                        }
                        else
                        {
                            points = ConnectorRouter.Route(parentPanel, begin, destConnPt);
                        }
                    }
                    else
                    {
                        // We are dragging the destination point of a connector.
                        ConnectionPoint srcConnPt = FreeFormPanel.GetSourceConnectionPoint(this.editedConnector);
                        if (targetConnPt != null)
                        {
                            points = ConnectorRouter.Route(parentPanel, srcConnPt, targetConnPt);
                            this.activeEditPoint.Location = targetConnPt.Location;
                        }
                        else
                        {
                            points = ConnectorRouter.Route(parentPanel, srcConnPt, end);
                        }
                    }
 
                    //When we start editing the end point we need to clear the slate and start over
                    List<EditPoint> newEditPoints = new List<EditPoint>();
                    if (points != null && points.Length > 1)
                    {
                        RemoveEditPoints(EditPoint.EditPointTypes.MultiSegmentEditPoint);
                        for (int i = 1; i < points.Length - 1; ++i)
                        {
                            newEditPoints.Add(new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, points[i]));
                        }
                        this.editPoints.InsertRange(1, newEditPoints.ToArray());
                    }
                }
                else if (this.activeEditPoint.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
                {
                    if (previous != null && previous.Type != EditPoint.EditPointTypes.ConnectionEditPoint && next != null && next.Type != EditPoint.EditPointTypes.ConnectionEditPoint)
                    {
                        //Update the previous point
                        double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, this.activeEditPoint.Location);
                        Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
                        previous.Location = (orientation == Orientation.Horizontal) ? new Point(previous.Location.X, newPoint.Y) : new Point(newPoint.X, previous.Location.Y);
 
                        //Update the next point
                        slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(this.activeEditPoint.Location, next.Location);
                        orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
                        next.Location = (orientation == Orientation.Horizontal) ? new Point(next.Location.X, newPoint.Y) : new Point(newPoint.X, next.Location.Y);
 
                        //Update the current point
                        this.activeEditPoint.Location = newPoint;
                    }
                    else
                    {
                        Fx.Assert(false, "Should not be here. UpdateEditPoints failed.");
                    }
                }
            }
 
            // Remove all the redundant edit points
            RemoveCoincidingEditPoints();
 
            bool validEditPoints = ValidateEditPoints();
            Fx.Assert(validEditPoints, "Validating EditPoints failed.");
        }
 
        bool ValidateEditPoints()
        {
            if (this.editPoints.Count < 2)
            {
                return false;
            }
 
            return true;
        }
 
        class EditPoint
        {
            EditPointTypes editPointType;
            Point point;
 
            public EditPoint(EditPointTypes editPointType, Point point)
            {
                this.editPointType = editPointType;
                this.point = point;
            }
 
            public Point Location
            {
                get
                {
                    return this.point;
                }
 
                set
                {
                    this.point = value;
                }
            }
 
            public EditPointTypes Type
            {
                get
                {
                    return this.editPointType;
                }
            }
 
            public enum EditPointTypes
            {
                ConnectionEditPoint = 1, MultiSegmentEditPoint
            }
        }
 
        sealed class EditPointAdorner : Adorner
        {
            ConnectorEditor adornedEditor;
            bool drawLines;
 
            public EditPointAdorner(ConnectorEditor cEditor, UIElement adornedElement, bool shouldDrawLines)
                : base(adornedElement)
            {
                Fx.Assert(adornedElement != null, "Adorned element is null.");
                adornedEditor = cEditor;
                this.IsHitTestVisible = false;
                this.drawLines = shouldDrawLines;
            }
 
            protected override void OnRender(DrawingContext drawingContext)
            {
                if (drawingContext != null)
                {
                    int i = 0;
                    SolidColorBrush renderBrush = new SolidColorBrush(WorkflowDesignerColors.WorkflowViewElementSelectedBackgroundColor);
                    renderBrush.Opacity = FreeFormPanel.ConnectorEditorOpacity;
                    Pen renderPen = new Pen(new SolidColorBrush(WorkflowDesignerColors.WorkflowViewElementSelectedBorderColor), FreeFormPanel.ConnectorEditorThickness);
                    double renderRadius = ConnectorEditor.EditPointRadius;
                    for (i = 0; i < adornedEditor.EditPoints.Count - 1; i++)
                    {
                        drawingContext.DrawEllipse(renderBrush, renderPen, adornedEditor.EditPoints[i].Location, renderRadius, renderRadius);
                        if (drawLines)
                        {
                            drawingContext.DrawLine(renderPen, adornedEditor.EditPoints[i].Location, adornedEditor.EditPoints[i + 1].Location);
                        }
                    }
                    drawingContext.DrawEllipse(renderBrush, renderPen, adornedEditor.EditPoints[i].Location, renderRadius, renderRadius);
                }
                base.OnRender(drawingContext);
                Keyboard.Focus(adornedEditor.Connector);
            }
        }
    }
}