Creating Edges
Edge creation is handled by CreateEdgeInputMode, mostly used as a child input mode of GraphEditorInputMode.
The actual edge creation is done in method createEdge which itself delegates to the callback set in edgeCreator. While it is possible to customize the actual edge creation by overriding createEdge setting a new edgeCreator should be preferred:
If prematureEndHitTestable is not changed the
edgeCreator's targetCandidate
parameter is
never null
.
During edge creation a preview of the new edge is shown. The IEdge instance which represents this preview edge can be retrieved via the dummyEdge property. You can change this preview edge (e.g. setting a different style, or adding labels) using the graph provided by dummyEdgeGraph.
By default, all properties of the preview edge are copied to the final edge that is created eventually. This includes bends, style, labels, and the edge’s tag. Thus, a viable option for customizing the created edge can also be to change the preview edge during the gesture, and have those changes copied automatically to the final edge. This has the benefit that the changes show up during the gesture, as opposed to changes done by the edgeCreator, which are only applied after the edge creation gesture is already complete:
The preview edge is reset to its default state after every edge creation gesture, so no further cleanup is necessary to prevent preview edge changes from persisting over multiple gestures.
The following events occur during an edge creation gesture (in the order they might occur):
Event | Occurs when … |
---|---|
Port Candidates and Port Candidate Providers
Fine-grained control where edges can and cannot connect during interactive edge creation is possible using port candidates: CreateEdgeInputMode retrieves an IPortCandidateProvider to find potential candidates for both source and target ports. The provider returns a set of port candidates which represent potential connection points for source and target ports for the edge to be created.
An IPortCandidate represents a potential port. Depending on the implementation this can be an existing port, but could also be a placeholder for a port yet to be created. Furthermore, an IPortCandidate has a validity that changes how (or if) edges can connect to it. CreateEdgeInputMode can potentially connect the edge to be created to a valid port candidate but not to an invalid one.
Note that invalid port candidates are visually indicated, thus providing a better user experience than simply not providing a port candidate. They can also help distinguishing the case “no edge may connect here” (no candidate) from “this edge may not connect here” (invalid candidate) — similar to disabled UI controls.
A third kind of validity is DYNAMIC: a dynamic port candidate has no fixed position but will be placed at the current (cursor) location. This usually has to be accompanied by a gesture, usually the Shift ⇧ key held down.
When edge creation is finished IPortCandidate’s port property is queried first. If the candidate represents an existing port, that port is used. If not, the createPort method is called by CreateEdgeInputMode to create and provide an appropriate port.
IPortCandidates are provided by an IPortCandidateProvider which is retrieved from the lookup of a potential port owner. During edge creation CreateEdgeInputMode calls the following methods:
Method | Description |
---|---|
Implementers have to implement above methods to return an appropriate list of candidates. Note that this list may be empty (or contain only invalid candidates) if an edge should not connect to the owner which is related to the provider.
The PortCandidateProviderBase class is a simplification of the IPortCandidateProvider interface. All its IPortCandidateProvider method implementations delegate to the getPortCandidates method and thus all return the same set of candidates. They can also be overridden to return something different, of course. Furthermore, PortCandidateProviderBase provides a set of convenience methods which you can use to easily create port candidates: the addExistingPorts method, and createCandidate.
The following example shows a simple port candidate provider which allows edges to connect to existing ports, but only if the source port has the same tag as the target port:
class SameTagPortCandidateProvider extends PortCandidateProviderBase {
// each instance is built for a specific port owner
/**
* @param {!INode} owner
*/
constructor(owner) {
super()
this.owner = owner
}
// the port candidate list which is returned by default
/**
* @param {!IInputModeContext} context
* @returns {!IEnumerable.<IPortCandidate>}
*/
getPortCandidates(context) {
// create a new list
const candidates = new List()
// add candidates for all existing ports
this.addExistingPorts(this.owner, candidates)
return candidates
}
// override this method to return candidates for each existing port
// but mark some as invalid
/**
* @param {!IInputModeContext} context
* @param {!IPortCandidate} source
* @returns {!IEnumerable.<IPortCandidate>}
*/
getTargetPortCandidates(context, source) {
return this.owner.ports
.map((port) => {
// create a candidate
const pc = new DefaultPortCandidate(port)
// mark it as invalid if the source port's user tag
// is not equal to the current port's user tag
pc.validity = port.tag === source.port.tag ? PortCandidateValidity.VALID : PortCandidateValidity.INVALID
return pc
})
.toList()
}
}
class SameTagPortCandidateProvider extends PortCandidateProviderBase {
// each instance is built for a specific port owner
constructor(private readonly owner: INode) {
super()
}
// the port candidate list which is returned by default
getPortCandidates(context: IInputModeContext): IEnumerable<IPortCandidate> {
// create a new list
const candidates = new List<IPortCandidate>()
// add candidates for all existing ports
this.addExistingPorts(this.owner, candidates)
return candidates
}
// override this method to return candidates for each existing port
// but mark some as invalid
getTargetPortCandidates(context: IInputModeContext, source: IPortCandidate): IEnumerable<IPortCandidate> {
return this.owner.ports
.map((port) => {
// create a candidate
const pc = new DefaultPortCandidate(port)
// mark it as invalid if the source port's user tag
// is not equal to the current port's user tag
pc.validity = port.tag === source.port!.tag ? PortCandidateValidity.VALID : PortCandidateValidity.INVALID
return pc
})
.toList()
}
}
The port owner’s lookup has to be modified to return an instance of this provider. You can do this using a NodeDecorator (or EdgeDecorator in case of edge to edge connections):
graph.decorator.nodeDecorator.portCandidateProviderDecorator.setFactory(
(node) => new SameTagPortCandidateProvider(node)
)
By default, a node returns a port candidate provider which returns all unoccupied ports if there is at least one, else it creates a new port candidate for the center of the node. yFiles for HTML already provides a number of IPortCandidateProvider implementations for the most common use cases which are mostly available through factory methods on the IPortCandidateProvider interface:
Factory Method | Description |
---|---|
The Port Candidate Provider demo shows how to implement IPortCandidateProvider for different purposes.
Showing Port Candidates while Creating Edges
CreateEdgeInputMode can display port candidates during edge creation. Its showPortCandidates property determines which port candidates should be displayed.
ShowPortCandidates value | Description |
---|---|
Setting this property to SOURCE or ALL will result in displaying source port candidates for each node the mouse is hovering over.
graphEditorInputMode.createEdgeInputMode.showPortCandidates = ShowPortCandidates.ALL
For a CreateEdgeInputMode as child of a default GraphEditorInputMode selected nodes will rather be moved than serve as source of an edge creation. There are two ways to solve this problem:
We can change the priority of the MoveInputMode lower (higher value) than the priority of the CreateEdgeInputMode and enable the startOverCandidateOnly property. Now you can create edges directly on the candidate. In places where there is no candidate, the selected node can be moved.
graphEditorInputMode.createEdgeInputMode.startOverCandidateOnly = true
graphEditorInputMode.createEdgeInputMode.priority = 40
graphEditorInputMode.moveInputMode.priority = 45
Or we simply do not show candidates on selected nodes with an appropriate hit testable …
class UnselectedNodesHitTestable extends BaseClass(IHitTestable) {
/**
* @param {!IInputModeContext} context
* @param {!Point} location
* @returns {boolean}
*/
isHit(context, location) {
const graph = context.graph
if (graph !== null) {
const graphComponent = context.canvasComponent
return graph.nodes.some(
(node) =>
!graphComponent.selection.isSelected(node) &&
node.style.renderer.getHitTestable(node, node.style).isHit(context, location)
)
}
return false
}
}
class UnselectedNodesHitTestable extends BaseClass(IHitTestable) implements IHitTestable {
isHit(context: IInputModeContext, location: Point): boolean {
const graph = context.graph
if (graph !== null) {
const graphComponent = context.canvasComponent as GraphComponent
return graph.nodes.some(
(node) =>
!graphComponent.selection.isSelected(node) &&
node.style.renderer.getHitTestable(node, node.style).isHit(context, location)
)
}
return false
}
}
… and set an instance to the properties showSourcePortCandidatesHitTestable and beginHitTestable.
const hitTestable = new UnselectedNodesHitTestable()
graphEditorInputMode.createEdgeInputMode.showSourcePortCandidatesHitTestable = hitTestable
graphEditorInputMode.createEdgeInputMode.beginHitTestable = hitTestable
Example: Triggering Edge Creation Gesture Programmatically
There are scenarios in which the edge creation gesture of the CreateEdgeInputMode should be triggered programmatically. For instance, it should start automatically after another action has finished, e.g., immediately after create a new node. Or it should start on a button click in the context menu or on the toolbar.
To start the edge creation programmatically, CreateEdgeInputMode provides the doStartEdgeCreation method.
In case the gesture based edge creation should be disabled, setting prepareRecognizer to NEVER is sufficient to disable it.
function configureInteraction() {
// register a GraphEditorInputMode with a CreateEdgeInputMode that does not allow gesture based edge creation
const inputMode = new GraphEditorInputMode()
inputMode.createEdgeInputMode.prepareRecognizer = EventRecognizers.NEVER
graphComponent.inputMode = inputMode
}
async function startEdgeCreation(inputMode, node) {
// starts edge creation on the given node
await inputMode.createEdgeInputMode.doStartEdgeCreation(
new DefaultPortCandidate(node, FreeNodePortLocationModel.NODE_CENTER_ANCHORED)
)
}
function configureInteraction(): void {
// register a GraphEditorInputMode with a CreateEdgeInputMode that does not allow gesture based edge creation
const inputMode = new GraphEditorInputMode()
inputMode.createEdgeInputMode.prepareRecognizer = EventRecognizers.NEVER
graphComponent.inputMode = inputMode
}
async function startEdgeCreation(inputMode: GraphEditorInputMode, node: INode): Promise<void> {
// starts edge creation on the given node
await inputMode.createEdgeInputMode.doStartEdgeCreation(
new DefaultPortCandidate(node, FreeNodePortLocationModel.NODE_CENTER_ANCHORED)
)
}
Example: Starting Edge Creation Inside the Source Node
By default, CreateEdgeInputMode starts edge creation gesture only after the mouse pointer leaves the source node. In some cases, it makes sense to start immediately when dragging inside the source node. This can be the case if edges should be created between two overlapping nodes, or for the creation of self-loops without the mouse having to leave and enter the node.
To configure when to start edge creation, CreateEdgeInputMode provides the property sourceNodeDraggingFinishedRecognizer.
To immediately start edge creation upon dragging from the source node, it is sufficient to assign ALWAYS.
function configureInteraction(graphComponent) {
// register a GraphEditorInputMode with a CreateEdgeInputMode that starts edge creation
// immediately inside the source node
const mode = new GraphEditorInputMode()
graphComponent.inputMode = mode
mode.createEdgeInputMode.sourceNodeDraggingFinishedRecognizer = EventRecognizers.ALWAYS
}
function configureInteraction(graphComponent: GraphComponent): void {
// register a GraphEditorInputMode with a CreateEdgeInputMode that starts edge creation
// immediately inside the source node
const mode = new GraphEditorInputMode()
graphComponent.inputMode = mode
mode.createEdgeInputMode.sourceNodeDraggingFinishedRecognizer = EventRecognizers.ALWAYS
}
Example: Configuring CreateEdgeInputMode to Create Sub Trees
In this example we demonstrate the use of various settings to configure CreateEdgeInputMode to a completely different behavior.
By default, CreateEdgeInputMode can only connect two existing nodes. It is, however, possible to let the edge creation end anywhere (“prematurely”) and create a new target node at the end point.
First, we configure CreateEdgeInputMode not to end at other nodes by setting the endHitTestable to IHitTestable.NEVER. Instead, we support ending at any location (“prematurely”) by setting the prematureEndHitTestable to IHitTestable.ALWAYS.
// never search for target ports
createEdgeInputMode.endHitTestable = IHitTestable.NEVER
// any location is a valid target location
createEdgeInputMode.prematureEndHitTestable = IHitTestable.ALWAYS
Since we don’t want to connect to other nodes we have to turn off snapping to targets. We also turn off showing possible targets.
// don't react to target ports
createEdgeInputMode.forceSnapToCandidate = false
createEdgeInputMode.snapToTargetCandidate = false
// don't even show them
createEdgeInputMode.showPortCandidates = ShowPortCandidates.NONE
createEdgeInputMode.allowSelfloops = false
// don't highlight target nodes
graphComponent.graph.decorator.nodeDecorator.highlightDecorator.hideImplementation()
Since we always create a new node at the target of the edge we want to show the new node already in the preview. Actually, we always have a target node during edge creation. It is, however, invisible because it has size (0,0) and the invisible VoidNodeStyle. To show the the preview target node we have to set the defaults of the dummyEdgeGraph:
// provide a default size
createEdgeInputMode.dummyEdgeGraph.nodeDefaults.size = graphComponent.graph.nodeDefaults.size
// set a style to become visible
createEdgeInputMode.dummyEdgeGraph.nodeDefaults.style = graphComponent.graph.nodeDefaults.style
Finally, we have to use an edgeCreator which does not only create a new edge but also a target node to connect the edge to:
// let the EdgeCreator create a new target node and connect the new edge to it
createEdgeInputMode.edgeCreator = (context, graph, sourcePortCandidate, targetPortCandidate, dummyEdge) => {
// copy the style from the dummy node
const dummyTargetNode = createEdgeInputMode.dummyTargetNode
const node = graph.createNode(dummyTargetNode.layout.toRect(), dummyTargetNode.style, dummyTargetNode.tag)
// create a port at the center
const targetPort = graph.addPort(node, createEdgeInputMode.dummyTargetNodePort.locationParameter)
// create the edge from the source port candidate to the new node
return graph.createEdge(sourcePortCandidate.createPort(context), targetPort, dummyEdge.style)
}
// let the EdgeCreator create a new target node and connect the new edge to it
createEdgeInputMode.edgeCreator = (
context: any,
graph: IGraph,
sourcePortCandidate: any,
targetPortCandidate: any,
dummyEdge: any
) => {
// copy the style from the dummy node
const dummyTargetNode = createEdgeInputMode.dummyTargetNode
const node = graph.createNode(dummyTargetNode.layout.toRect(), dummyTargetNode.style, dummyTargetNode.tag)
// create a port at the center
const targetPort = graph.addPort(node, createEdgeInputMode.dummyTargetNodePort.locationParameter)
// create the edge from the source port candidate to the new node
return graph.createEdge(sourcePortCandidate.createPort(context), targetPort, dummyEdge.style)
}
That’s it: now the re-configured CreateEdgeInputMode doesn’t connect two existing nodes anymore. Instead, it creates a new connected node.
To run a layout after each edge creation you can listen to the EdgeCreated event.