I finally found some time to do the port. I have not tested this thoroughly. This is in two parts. Part #1, C# code:
/* Copyright © Northwoods Software Corporation, 2008-2025. All Rights Reserved. */
using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using System.Windows.Shapes;
using WinRT;
//using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Guidelines {
public partial class Guidelines : Window {
public Guidelines() {
InitializeComponent();
var model = new GraphLinksModel<NodeInfo, String, String, UniversalLinkData>();
model.NodesSource = new ObservableCollection<NodeInfo>() {
new NodeInfo() { Key="A", Color="LightBlue", Location=new Point(0, 0) },
new NodeInfo() { Key="B", Color="Yellow", Location=new Point(200, 0) },
new NodeInfo() { Key="C", Color="LightGreen", Location=new Point(400, 0) },
new NodeInfo() { Key="D", Color="Pink", Location=new Point(0, 200) },
new NodeInfo() { Key="E", Color="Orange", Location=new Point(200, 200) },
new NodeInfo() { Key="F", Color="Tomato", Location=new Point(400, 210) },
new NodeInfo() { Key="G", Color="Lavender", Location=new Point(0, 400) },
new NodeInfo() { Key="H", Color="Aqua", Location=new Point(210, 400) },
new NodeInfo() { Key="I", Color="Teal", Location=new Point(400, 400) },
};
model.Modifiable = true;
model.HasUndoManager = true;
myDiagram.Model = model;
myDiagram.DraggingTool = new GuidedDraggingTool();
}
}
public class NodeInfo : GraphLinksModelNodeData<String> {
public String Color {
get { return _Color; }
set { if (_Color != value) { String old = _Color; _Color = value; RaisePropertyChanged("Color", old, value); } }
}
private String _Color = "Fuchsia";
public double Width {
get { return _Width; }
set { if (_Width != value) { double old = _Width; _Width = value; RaisePropertyChanged("Width", old, value); } }
}
private double _Width = 60;
public double Height {
get { return _Height; }
set { if (_Height != value) { double old = _Height; _Height = value; RaisePropertyChanged("Height", old, value); } }
}
private double _Height = 60;
}
/**
* The GuidedDraggingTool class makes guidelines visible as the parts are dragged around a diagram
* when the selected part is nearly aligned with another part, or when a part is nearly positioned
* between two other parts with equal space on both sides.
*
* During dragging, this tool will show temporary Parts named "guide..." to indicate what the dragged Node will align with.
* You can customize the appearance of those Parts by setting those properties.
* By default they are Parts in the "Tool" Layer holding magenta or cyan dashed lines.
* You may also set some of those "guide..." properties to null if you do not want those guides to be shown
* and do not want to snap to those alignments.
*
* Normally as the user drags a Node, that Node will snap to center itself with equal spacing on both sides,
* or it will line up with a nearby Node. If you do not want that snapping behavior during a drag,
* the user can hold down the Shift modifier key in order to move the Node smoothly,
* or you can set {@link IsRealtimeSnapEnabled} to false.
* When that property is set to false, snapping will still happen upon mouse-up.
*
* You can set the {@link IsGuidelineSnapEnabled} or {@link IsEqualSpacingSnapEnabled} property to false
* to avoid that kind of snapping behavior.
* When both those properties are true, as they are by default,
* and when a Node is near a point that provides both equal spacing between two Parts and alignment with a nearby Node,
* the equal spacing snapping takes precedence.
*
* The maximum distance from perfect algnment that a dragged Node will snap to is controlled by the
* {@link guideSnapDistance} property. This tends to be a small value.
*
* The maximum distance at which another Node might affect the alignment of the dragged Node is controlled by the
* {@link searchDistance} property. This tends to be a large value.
*
* If you want to experiment with this extension, try the <a href="../../samples/GuidedDragging.html">Guided Dragging</a> sample.
* @category Tool Extension
*/
public class GuidedDraggingTool : DraggingTool {
/**
* Constructs a GuidedDraggingTool and sets up the temporary guideline parts.
*/
public GuidedDraggingTool() {
this.Name = "GuidedDragging";
this.IsRealtimeSnapEnabled = true;
this.IsGuidelineSnapEnabled = true;
this.IsEqualSpacingSnapEnabled = true;
this.GuideSnapDistance = 6;
this.SearchDistance = 2000;
this.ShowsGuides = true;
// temporary parts for horizonal guidelines
this.GuidelineHTop = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineHTop.Content = new Canvas();
(this.GuidelineHTop.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
this.GuidelineHCenter = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineHCenter.Content = new Canvas();
(this.GuidelineHCenter.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
this.GuidelineHBottom = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineHBottom.Content = new Canvas();
(this.GuidelineHBottom.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
// temporary parts for vertical guidelines
this.GuidelineVLeft = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineVLeft.Content = new Canvas();
(this.GuidelineVLeft.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
this.GuidelineVCenter = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineVCenter.Content = new Canvas();
(this.GuidelineVCenter.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
this.GuidelineVRight = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuidelineVRight.Content = new Canvas();
(this.GuidelineVRight.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
// temporary parts for spacing guides
this.GuideHSpacingLeft = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuideHSpacingLeft.Content = new Canvas();
(this.GuideHSpacingLeft.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
(this.GuideHSpacingLeft.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
this.GuideHSpacingRight = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuideHSpacingRight.Content = new Canvas();
(this.GuideHSpacingRight.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
(this.GuideHSpacingRight.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
this.GuideVSpacingTop = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuideVSpacingTop.Content = new Canvas();
(this.GuideVSpacingTop.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
(this.GuideVSpacingTop.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
this.GuideVSpacingBottom = new Node() { LayerName = "Tool", InDiagramBounds = false };
this.GuideVSpacingBottom.Content = new Canvas();
(this.GuideVSpacingBottom.Content as Canvas).Children
.Add(
new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
);
(this.GuideVSpacingBottom.Content as Canvas).Children
.Add(
new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
);
}
/**
* Gets or sets the margin of error for which guidelines show up.
*
* The default value is 6.
* Guidelines will show up when the aligned nodes are ± 6px away from perfect alignment.
*/
public double GuideSnapDistance { get; set; }
/**
* Gets or sets the distance around the selected part to search for aligned parts.
*
* The default value is 2000.
* Set this to Infinity if you want to search the entire diagram no matter how far away.
*/
public double SearchDistance { get; set; }
/**
* Gets or sets whether snapping for equal spacing or to guidelines is enabled during a drag.
* This property is useless when both {@link IsGuidelineSnapEnabled} and {@link isEqualSpacingSnapEnabled} are false.
*
* The default value is true.
*/
public bool IsRealtimeSnapEnabled{ get; set; }
/**
* Gets or sets whether snapping to guidelines provided by nearby Parts is enabled.
*
* The default value is true.
*/
public bool IsGuidelineSnapEnabled { get; set; }
/**
* Gets or sets whether snapping to have equal space on both sides of the moved Node is enabled.
*
* The default value is true.
*/
public bool IsEqualSpacingSnapEnabled { get; set; }
/**
* Gets or sets whether the guidelines are shown or not.
* Setting this to false causes no horizontal or vertical
* guidelines, nor the spacing guides, to be shown.
* However, snapping may still take place, during the drag if {@link IsRealtimeSnapEnabled} is true,
* or at the end on mouse-up.
*
* The default value is true.
*/
public bool ShowsGuides { get; set; }
/**
* Gets or sets the Node to show when the moved Node's top lines up with a stationary Node.
* This defaults to a horizontal magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineHTop { get; set; }
/**
* Gets or sets the Node to show when the moved Node's center lines up with a stationary Node.
* This defaults to a horizontal magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineHCenter { get; set; }
/**
* Gets or sets the Node to show when the moved Node's bottom lines up with a stationary Node.
* This defaults to a horizontal magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineHBottom { get; set; }
/**
* Gets or sets the Node to show when the moved Node's left side lines up with a stationary Node.
* This defaults to a vertical magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineVLeft { get; set; }
/**
* Gets or sets the Node to show when the moved Node's center lines up with a stationary Node.
* This defaults to a vertical magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineVCenter { get; set; }
/**
* Gets or sets the Node to show when the moved Node's right side lines up with a stationary Node.
* This defaults to a vertical magenta dashed line. Set this to null to not show anything for this case.
* */
public Node GuidelineVRight { get; set; }
/**
* Gets or sets the Node to show on the left side when the moved Node leaves nearly the same spacing left and right
* between this Node and the nearest Parts on either side of it.
* This defaults to a vertical cyan dashed line. Set this to null to not show anything for this case.
* */
public Node GuideHSpacingLeft { get; set; }
/**
* Gets or sets the Node to show on the right side when the moved Node leaves nearly the same spacing left and right
* between this Node and the nearest Parts on either side of it.
* This defaults to a vertical cyan dashed line. Set this to null to not show anything for this case.
* */
public Node GuideHSpacingRight { get; set; }
/**
* Gets or sets the Node to show above when the moved Node leaves nearly the same spacing above and below
* between this Node and the nearest Parts above and below it.
* This defaults to a vertical cyan dashed line. Set this to null to not show anything for this case.
* */
public Node GuideVSpacingTop { get; set; }
/**
* Gets or sets the Node to show below when the moved Node leaves nearly the same spacing above and below
* between this Node and the nearest Parts above and below it.
* This defaults to a vertical cyan dashed line. Set this to null to not show anything for this case.
* */
public Node GuideVSpacingBottom { get; set; }
/**
* Removes all of the guidelines from the grid.
*/
public void ClearGuidelines() {
if (this.GuidelineHTop != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHTop);
if (this.GuidelineHCenter != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHCenter);
if (this.GuidelineHBottom != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHBottom);
if (this.GuidelineVLeft != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVLeft);
if (this.GuidelineVCenter != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVCenter);
if (this.GuidelineVRight != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVRight);
if (this.GuideHSpacingLeft != null) this.Diagram.PartsModel.RemoveNode(this.GuideHSpacingLeft);
if (this.GuideHSpacingRight != null) this.Diagram.PartsModel.RemoveNode(this.GuideHSpacingRight);
if (this.GuideVSpacingTop != null) this.Diagram.PartsModel.RemoveNode(this.GuideVSpacingTop);
if (this.GuideVSpacingBottom != null) this.Diagram.PartsModel.RemoveNode(this.GuideVSpacingBottom);
}
/**
* Calls the base method and removes the guidelines from the graph.
*/
override public void DoDeactivate() {
// clear any guidelines when dragging is done
this.ClearGuidelines();
base.DoDeactivate();
}
/**
* Shows vertical and horizontal guidelines for the dragged part.
*/
override protected void DragOver(Point pt, bool moving, bool copying) {
// clear all existing guidelines in case either show... method decides to show a guideline
this.ClearGuidelines();
// gets the selected part
var draggingParts = this.CopiedParts;
if (draggingParts == null) draggingParts = this.DraggedParts;
if (draggingParts == null) return;
foreach (Part part in draggingParts.Keys) {
var node = part as Node;
if (node == null) continue;
// maybe snaps during drag
var snap = this.IsRealtimeSnapEnabled && !IsShiftKeyDown();
this.ShowMatches(node, this.ShowsGuides, snap);
}
}
/**
* On a mouse-up, snaps the selected part to the nearest guideline.
* If not snapping, the part remains at its position.
*/
override protected void DropOnto(Point pt) {
this.ClearGuidelines();
// gets the selected (perhaps copied) Node
var draggingParts = this.CopiedParts;
if (draggingParts == null) draggingParts = this.DraggedParts;
if (draggingParts == null) return;
foreach (Part part in draggingParts.Keys) {
var node = part as Node;
if (node == null) continue;
// snaps only when the mouse is released without shift modifier
var snap = !IsShiftKeyDown();
this.ShowMatches(node, false, snap); // false means don't show guidelines
}
}
/**
* When nodes are shifted due to being guided upon a drop, make sure all connected link routes are invalidated,
* since the node is likely to have moved a different amount than all its connected links in the regular
* operation of the DraggingTool.
*/
void InvalidateLinks(Node node){
if (node is Node) node.InvalidateRelationships();
}
/**
* This predicate decides whether or not the given Node should guide the dragged part.
* @param part - a stationary Node to which the dragged part might be aligned
* @param guidedpart - the Node being dragged
*/
protected bool IsGuiding(Node part, Node guidedpart) {
return (
!part.IsSelected &&
guidedpart is Node &&
part.ContainingSubGraph == guidedpart.ContainingSubGraph &&
part.Layer != null &&
!part.Layer.IsTemporary
);
}