//------------------------------------------------------------ // 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 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(); 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 EditPoints { get { return this.editPoints; } } public List 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 segments = new List(); 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 segments = new List(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 GetPointsFromEditPoints() { List segments = new List(); //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 editPointsToRetain = new List(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 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 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 editPointsToRemove = new List(); 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 newEditPoints = new List(); 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); } } } }