1 //------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 //------------------------------------------------------------
5 namespace System.Activities.Presentation.FreeFormEditing
8 using System.Activities.Presentation;
9 using System.Collections.Generic;
10 using System.Diagnostics;
12 using System.Windows.Controls;
13 using System.Windows.Documents;
14 using System.Windows.Input;
16 using System.Diagnostics.CodeAnalysis;
17 using System.Windows.Media;
18 using System.Activities.Presentation.Internal.PropertyEditing;
20 internal class ConnectorEditor
22 const double EditPointRadius = 4;
23 const double EditPointHitTestRadius = 9;
24 const int minLengthForSegmentEditPoint = 10;
25 EditPoint activeEditPoint;
26 AdornerLayer adornerLayer;
27 Connector editedConnector;
28 List<EditPoint> editPoints;
29 FreeFormPanel parentPanel;
31 public ConnectorEditor(FreeFormPanel panel, Connector connector)
35 throw FxTrace.Exception.AsError(new ArgumentNullException("panel"));
37 if (connector == null)
39 throw FxTrace.Exception.AsError(new ArgumentNullException("connector"));
41 this.editPoints = new List<EditPoint>();
42 this.parentPanel = panel;
43 this.editedConnector = connector;
44 this.activeEditPoint = null;
45 connector.IsSelected = true;
46 // When the ConnectorEditor is active, we allow reconnecting the start point of the Connector instead
47 // of creating a new transition that shares the same trigger. So we need to disable tooltips and
48 // highlighting effects for all overlapping start dots.
49 this.SetIsHitTestVisibleForOverlappingStartDots(false);
52 public bool BeingEdited
56 return (this.activeEditPoint != null);
60 public bool IsConnectorStartBeingMoved
64 return (this.BeingEdited && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint
65 && this.editedConnector.Points[0] != this.EditPoints[0].Location);
69 public bool IsConnectorEndBeingMoved
73 return (this.BeingEdited && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint
74 && this.editedConnector.Points[this.editedConnector.Points.Count - 1] != this.EditPoints[this.EditPoints.Count - 1].Location);
78 public Connector Connector
82 return this.editedConnector;
87 this.editedConnector = value;
91 List<EditPoint> EditPoints
95 return this.editPoints;
99 public List<Point> ConnectorEditorLocation
103 return this.GetPointsFromEditPoints();
107 void SetIsHitTestVisibleForOverlappingStartDots(bool hitTestVisible)
109 ConnectionPoint srcConnectionPoint = FreeFormPanel.GetSourceConnectionPoint(this.Connector);
110 foreach (Connector overlappingConnector in srcConnectionPoint.AttachedConnectors)
112 if (overlappingConnector.StartDot != null)
114 overlappingConnector.StartDot.IsHitTestVisible = hitTestVisible;
119 //If the result is true this method also sets the currently active edit point.
120 public bool EditPointsHitTest(Point pt)
122 if (this.EditPoints.Count > 0)
124 foreach (EditPoint editPoint in this.EditPoints)
126 if (DesignerGeometryHelper.DistanceBetweenPoints(pt, editPoint.Location) <= EditPointHitTestRadius)
128 this.activeEditPoint = editPoint;
136 //Connector editing is completed. This function saves the state of the connectorEditor into the corresponding connector.
137 //Returns whether the Editor was persisted or not. It might not be persisted if Connector end points do not lie on a designer.
138 public bool Persist(Point finalSnappedPoint)
140 List<Point> segments = new List<Point>();
141 this.Update(finalSnappedPoint);
142 if (this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
146 segments = this.GetPointsFromEditPoints();
147 this.parentPanel.UpdateConnectorPoints(Connector, segments);
148 this.activeEditPoint = null;
154 //The Connector editor is to be destroyed. Remove the adorners on the editor. activeEditPoint=null sets BeingEdited property to false.
157 this.activeEditPoint = null;
159 this.EditPoints.Clear();
160 this.Connector.IsSelected = false;
161 // Restore the IsHitTestVisible property
162 this.SetIsHitTestVisibleForOverlappingStartDots(true);
163 this.Connector = null;
164 this.parentPanel = null;
167 //This method removes the existing adorner on the edited connector, updates the active edit points and creates new adorners.
168 public void Update(Point newPoint)
171 UpdateEditPoints(newPoint);
172 Fx.Assert(this.activeEditPoint != null, "activeEditPoint is null");
173 adornerLayer.Add(new EditPointAdorner(this, editedConnector, true));
176 //Add edit points of specified type
177 void AddEditPoints(EditPoint.EditPointTypes editPointType)
179 if (editPointType == EditPoint.EditPointTypes.ConnectionEditPoint)
181 if (this.editPoints.Count == 0 || !this.editPoints[0].Location.Equals(editedConnector.Points[0]))
183 this.editPoints.Insert(0, new EditPoint(EditPoint.EditPointTypes.ConnectionEditPoint, editedConnector.Points[0]));
186 if (this.editPoints.Count < 2 || !this.editPoints[this.editPoints.Count - 1].Equals(editedConnector.Points[editedConnector.Points.Count - 1]))
188 editPoints.Add(new EditPoint(EditPoint.EditPointTypes.ConnectionEditPoint, editedConnector.Points[editedConnector.Points.Count - 1]));
191 else if (editPointType == EditPoint.EditPointTypes.MultiSegmentEditPoint)
193 if (this.editPoints.Count == 2)
195 List<Point> segments = new List<Point>(this.editedConnector.Points);
196 if (segments.Count > 0)
198 segments.RemoveAt(0);
199 segments.RemoveAt(segments.Count - 1);
202 for (int i = 0; i < segments.Count; i++)
204 this.editPoints.Insert(this.editPoints.Count - 1, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, segments[i]));
209 Fx.Assert(false, "EditPoints.Count is not 2.");
214 void CreateEditPoints()
216 this.editPoints.Clear();
218 AddEditPoints(EditPoint.EditPointTypes.ConnectionEditPoint);
219 AddEditPoints(EditPoint.EditPointTypes.MultiSegmentEditPoint);
221 bool validEditPoints = ValidateEditPoints();
222 Fx.Assert(validEditPoints, "Validating EditPoints failed.");
225 void DisplayEditPoints()
228 adornerLayer = AdornerLayer.GetAdornerLayer(editedConnector);
229 if (adornerLayer != null)
231 adornerLayer.Add(new EditPointAdorner(this, editedConnector, false));
235 List<Point> GetPointsFromEditPoints()
237 List<Point> segments = new List<Point>();
238 //Connection end points will never be moved/removed in following two function calls. Hence passing null as pointsToRetain.
239 RemoveEditPointSegmentsWithinTolerance(null);
240 RemoveCoincidingEditPoints(null);
241 for (int i = 0; i < this.EditPoints.Count; i++)
243 segments.Add(this.EditPoints[i].Location);
248 void RemoveAdorners()
250 if (adornerLayer != null && editedConnector != null)
252 Adorner[] adorners = adornerLayer.GetAdorners(editedConnector);
253 if (adorners != null)
255 foreach (Adorner adorner in adorners)
257 adornerLayer.Remove(adorner);
263 //Remove points with the same slope
264 void RemoveCoincidingEditPoints()
266 if (this.editPoints.Count < 2 ||
267 this.editPoints[0].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
268 this.editPoints[this.editPoints.Count - 1].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
269 (this.activeEditPoint != null && this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint))
274 //Create list of points to retain
275 List<EditPoint> editPointsToRetain = new List<EditPoint>(this.editPoints.Count);
276 for (int i = 0; i < this.editPoints.Count; i++)
278 if (this.editPoints[i].Type != EditPoint.EditPointTypes.MultiSegmentEditPoint ||
279 this.editPoints[i] == this.activeEditPoint)
281 editPointsToRetain.Add(this.editPoints[i]);
285 //Step1: Get rid of all the line segments which are within tolerance range
286 RemoveEditPointSegmentsWithinTolerance(editPointsToRetain);
288 //Step2: We should make sure that the active edit point is always retained but those points which are coincidental are always removed
289 RemoveCoincidingEditPoints(editPointsToRetain);
291 //Step3: Go through each segment and ensure that all the segments are either vertical or horizontal
292 for (int i = 0; i < this.editPoints.Count - 1; i++)
294 EditPoint current = this.editPoints[i];
295 EditPoint next = this.editPoints[i + 1];
297 double slope = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
298 if (slope != 0 && slope != double.MaxValue)
300 Point location = (slope < 1) ? new Point(next.Location.X, current.Location.Y) : new Point(current.Location.X, next.Location.Y);
301 this.editPoints.Insert(i + 1, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, location));
306 void RemoveEditPointSegmentsWithinTolerance(List<EditPoint> pointsToRetain)
308 for (int i = 1; i < this.editPoints.Count - 1; i++)
310 EditPoint previous = this.editPoints[i - 1];
311 EditPoint current = this.editPoints[i];
312 EditPoint next = this.editPoints[i + 1];
314 if (pointsToRetain == null || !pointsToRetain.Contains(current))
316 double distance = DesignerGeometryHelper.DistanceOfLineSegments(new Point[] { previous.Location, current.Location });
317 if (distance < ConnectorEditor.EditPointRadius && next.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
319 double slope = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
320 next.Location = (slope < 1) ? new Point(next.Location.X, previous.Location.Y) : new Point(previous.Location.X, next.Location.Y);
321 this.editPoints.Remove(current);
326 distance = DesignerGeometryHelper.DistanceOfLineSegments(new Point[] { current.Location, next.Location });
327 if (distance < ConnectorEditor.EditPointRadius && previous.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
329 double slope = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, current.Location);
330 previous.Location = (slope < 1) ? new Point(previous.Location.X, next.Location.Y) : new Point(next.Location.X, previous.Location.Y);
331 this.editPoints.Remove(current);
340 void RemoveCoincidingEditPoints(List<EditPoint> pointsToRetain)
342 for (int i = 1; i < this.EditPoints.Count - 1; i++)
344 EditPoint current = this.EditPoints[i];
345 if (pointsToRetain == null || !pointsToRetain.Contains(current))
347 EditPoint previous = this.EditPoints[i - 1];
348 EditPoint next = this.EditPoints[i + 1];
349 double slope1 = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, current.Location);
350 double slope2 = DesignerGeometryHelper.SlopeOfLineSegment(current.Location, next.Location);
351 if (Math.Abs(slope1) == Math.Abs(slope2))
353 this.EditPoints.Remove(current);
360 //Remove edit points of specified type
361 //This method does not remove this.activeEditPoint.
362 void RemoveEditPoints(EditPoint.EditPointTypes editPointType)
364 List<EditPoint> editPointsToRemove = new List<EditPoint>();
365 for (int i = 0; i < this.editPoints.Count; i++)
367 EditPoint editPoint = this.editPoints[i];
368 if (editPoint.Type == editPointType)
370 editPointsToRemove.Add(editPoint);
374 for (int i = 0; i < editPointsToRemove.Count; i++)
376 EditPoint editPoint = editPointsToRemove[i];
377 if (editPoint != this.activeEditPoint)
379 this.editPoints.Remove(editPoint);
384 [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a legacy algorithm.")]
385 void UpdateEditPoints(Point newPoint)
387 if (this.editPoints.Count < 2 ||
388 this.editPoints[0].Type != EditPoint.EditPointTypes.ConnectionEditPoint ||
389 this.editPoints[this.editPoints.Count - 1].Type != EditPoint.EditPointTypes.ConnectionEditPoint)
391 Fx.Assert(false, "EditPoints are invalid");
395 if (this.activeEditPoint != null)
397 int activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
398 EditPoint previous = (activeEditPointIndex > 0) ? this.editPoints[activeEditPointIndex - 1] : null;
399 EditPoint next = (activeEditPointIndex < this.editPoints.Count - 1) ? this.editPoints[activeEditPointIndex + 1] : null;
401 //Note that extra edit points are only added if we are connected to connection point
402 if (previous != null && previous.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
404 double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, this.activeEditPoint.Location);
405 Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
407 int editPointOffset = Convert.ToInt32(DesignerGeometryHelper.DistanceBetweenPoints(previous.Location, (next != null) ? next.Location : this.activeEditPoint.Location)) / 4;
408 if (orientation == Orientation.Horizontal)
410 editPointOffset *= (previous.Location.X < this.activeEditPoint.Location.X) ? 1 : -1;
414 editPointOffset *= (previous.Location.Y < this.activeEditPoint.Location.Y) ? 1 : -1;
417 activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
418 Point editPointLocation = (orientation == Orientation.Horizontal) ? new Point(previous.Location.X + editPointOffset, previous.Location.Y) : new Point(previous.Location.X, previous.Location.Y + editPointOffset);
419 previous = new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation);
420 this.editPoints.InsertRange(activeEditPointIndex, new EditPoint[] { new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation), previous });
423 if (next != null && next.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
425 double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(this.activeEditPoint.Location, next.Location);
426 Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
428 int editPointOffset = Convert.ToInt32(DesignerGeometryHelper.DistanceBetweenPoints((previous != null) ? previous.Location : this.activeEditPoint.Location, next.Location)) / 4;
429 if (orientation == Orientation.Horizontal)
431 editPointOffset *= (this.activeEditPoint.Location.X < next.Location.X) ? -1 : 1;
435 editPointOffset *= (this.activeEditPoint.Location.Y < next.Location.Y) ? -1 : 1;
438 activeEditPointIndex = this.editPoints.IndexOf(this.activeEditPoint);
439 Point editPointLocation = (orientation == Orientation.Horizontal) ? new Point(next.Location.X + editPointOffset, next.Location.Y) : new Point(next.Location.X, next.Location.Y + editPointOffset);
440 next = new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation);
441 this.editPoints.InsertRange(activeEditPointIndex + 1, new EditPoint[] { next, new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, editPointLocation) });
444 if (this.activeEditPoint.Type == EditPoint.EditPointTypes.ConnectionEditPoint)
446 Fx.Assert(this.editPoints[0].Type == EditPoint.EditPointTypes.ConnectionEditPoint, "EditPoint type is wrong.");
447 Fx.Assert(this.editPoints[editPoints.Count - 1].Type == EditPoint.EditPointTypes.ConnectionEditPoint, "EditPoint type is wrong.");
448 this.activeEditPoint.Location = newPoint;
450 Fx.Assert(this.editPoints.Count > 0, "Some edit point should exist");
451 ConnectionPoint targetConnPt = null;
452 Point[] points = null;
453 Point begin = this.editPoints[0].Location;
454 Point end = this.editPoints[this.editPoints.Count - 1].Location;
456 if (typeof(ConnectionPointsAdorner).IsAssignableFrom(Mouse.DirectlyOver.GetType()))
458 ConnectionPointsAdorner connPtsAdorner = Mouse.DirectlyOver as ConnectionPointsAdorner;
459 targetConnPt = FreeFormPanel.ConnectionPointHitTest(newPoint, connPtsAdorner);
462 if (activeEditPointIndex == 0)
464 // We are dragging the source point of a connector.
465 ConnectionPoint destConnPt = FreeFormPanel.GetDestinationConnectionPoint(this.editedConnector);
466 if (targetConnPt != null)
468 points = ConnectorRouter.Route(parentPanel, targetConnPt, destConnPt);
469 this.activeEditPoint.Location = targetConnPt.Location;
473 points = ConnectorRouter.Route(parentPanel, begin, destConnPt);
478 // We are dragging the destination point of a connector.
479 ConnectionPoint srcConnPt = FreeFormPanel.GetSourceConnectionPoint(this.editedConnector);
480 if (targetConnPt != null)
482 points = ConnectorRouter.Route(parentPanel, srcConnPt, targetConnPt);
483 this.activeEditPoint.Location = targetConnPt.Location;
487 points = ConnectorRouter.Route(parentPanel, srcConnPt, end);
491 //When we start editing the end point we need to clear the slate and start over
492 List<EditPoint> newEditPoints = new List<EditPoint>();
493 if (points != null && points.Length > 1)
495 RemoveEditPoints(EditPoint.EditPointTypes.MultiSegmentEditPoint);
496 for (int i = 1; i < points.Length - 1; ++i)
498 newEditPoints.Add(new EditPoint(EditPoint.EditPointTypes.MultiSegmentEditPoint, points[i]));
500 this.editPoints.InsertRange(1, newEditPoints.ToArray());
503 else if (this.activeEditPoint.Type == EditPoint.EditPointTypes.MultiSegmentEditPoint)
505 if (previous != null && previous.Type != EditPoint.EditPointTypes.ConnectionEditPoint && next != null && next.Type != EditPoint.EditPointTypes.ConnectionEditPoint)
507 //Update the previous point
508 double slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(previous.Location, this.activeEditPoint.Location);
509 Orientation orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
510 previous.Location = (orientation == Orientation.Horizontal) ? new Point(previous.Location.X, newPoint.Y) : new Point(newPoint.X, previous.Location.Y);
512 //Update the next point
513 slopeOfLine = DesignerGeometryHelper.SlopeOfLineSegment(this.activeEditPoint.Location, next.Location);
514 orientation = (Math.Abs(slopeOfLine) < 1) ? Orientation.Horizontal : Orientation.Vertical;
515 next.Location = (orientation == Orientation.Horizontal) ? new Point(next.Location.X, newPoint.Y) : new Point(newPoint.X, next.Location.Y);
517 //Update the current point
518 this.activeEditPoint.Location = newPoint;
522 Fx.Assert(false, "Should not be here. UpdateEditPoints failed.");
527 // Remove all the redundant edit points
528 RemoveCoincidingEditPoints();
530 bool validEditPoints = ValidateEditPoints();
531 Fx.Assert(validEditPoints, "Validating EditPoints failed.");
534 bool ValidateEditPoints()
536 if (this.editPoints.Count < 2)
546 EditPointTypes editPointType;
549 public EditPoint(EditPointTypes editPointType, Point point)
551 this.editPointType = editPointType;
555 public Point Location
568 public EditPointTypes Type
572 return this.editPointType;
576 public enum EditPointTypes
578 ConnectionEditPoint = 1, MultiSegmentEditPoint
582 sealed class EditPointAdorner : Adorner
584 ConnectorEditor adornedEditor;
587 public EditPointAdorner(ConnectorEditor cEditor, UIElement adornedElement, bool shouldDrawLines)
588 : base(adornedElement)
590 Fx.Assert(adornedElement != null, "Adorned element is null.");
591 adornedEditor = cEditor;
592 this.IsHitTestVisible = false;
593 this.drawLines = shouldDrawLines;
596 protected override void OnRender(DrawingContext drawingContext)
598 if (drawingContext != null)
601 SolidColorBrush renderBrush = new SolidColorBrush(WorkflowDesignerColors.WorkflowViewElementSelectedBackgroundColor);
602 renderBrush.Opacity = FreeFormPanel.ConnectorEditorOpacity;
603 Pen renderPen = new Pen(new SolidColorBrush(WorkflowDesignerColors.WorkflowViewElementSelectedBorderColor), FreeFormPanel.ConnectorEditorThickness);
604 double renderRadius = ConnectorEditor.EditPointRadius;
605 for (i = 0; i < adornedEditor.EditPoints.Count - 1; i++)
607 drawingContext.DrawEllipse(renderBrush, renderPen, adornedEditor.EditPoints[i].Location, renderRadius, renderRadius);
610 drawingContext.DrawLine(renderPen, adornedEditor.EditPoints[i].Location, adornedEditor.EditPoints[i + 1].Location);
613 drawingContext.DrawEllipse(renderBrush, renderPen, adornedEditor.EditPoints[i].Location, renderRadius, renderRadius);
615 base.OnRender(drawingContext);
616 Keyboard.Focus(adornedEditor.Connector);