8343705: IGV: Interactive Node Moving in Hierarchical Layout

Reviewed-by: chagedorn, thartmann, rcastanedalo
This commit is contained in:
Tobias Holenstein 2024-11-29 15:16:53 +00:00
parent 4da7c35484
commit 28b0f3eaa5
9 changed files with 1298 additions and 15 deletions

View file

@ -33,7 +33,7 @@ import java.util.*;
*
* @author Thomas Wuerthinger
*/
public class HierarchicalLayoutManager extends LayoutManager {
public class HierarchicalLayoutManager extends LayoutManager implements LayoutMover {
int maxLayerLength;
private LayoutGraph graph;
@ -77,6 +77,62 @@ public class HierarchicalLayoutManager extends LayoutManager {
graph = layoutGraph;
}
@Override
public void moveLink(Point linkPos, int shiftX) {
int layerNr = graph.findLayer(linkPos.y);
for (LayoutNode node : graph.getLayer(layerNr)) {
if (node.isDummy() && linkPos.x == node.getX()) {
LayoutLayer layer = graph.getLayer(layerNr);
if (layer.contains(node)) {
node.setX(linkPos.x + shiftX);
layer.sortNodesByX();
break;
}
}
}
writeBack();
}
@Override
public void moveVertices(Set<? extends Vertex> movedVertices) {
for (Vertex vertex : movedVertices) {
moveVertex(vertex);
}
writeBack();
}
private void writeBack() {
graph.optimizeBackEdgeCrossings();
graph.updateLayerMinXSpacing();
graph.straightenEdges();
WriteResult.apply(graph);
}
@Override
public void moveVertex(Vertex movedVertex) {
Point newLoc = movedVertex.getPosition();
LayoutNode movedNode = graph.getLayoutNode(movedVertex);
assert !movedNode.isDummy();
int layerNr = graph.findLayer(newLoc.y + movedNode.getOuterHeight() / 2);
if (movedNode.getLayer() == layerNr) { // we move the node in the same layer
LayoutLayer layer = graph.getLayer(layerNr);
if (layer.contains(movedNode)) {
movedNode.setX(newLoc.x);
layer.sortNodesByX();
}
} else { // only remove edges if we moved the node to a new layer
if (maxLayerLength > 0) return; // TODO: not implemented
graph.removeNodeAndEdges(movedNode);
layerNr = graph.insertNewLayerIfNeeded(movedNode, layerNr);
graph.addNodeToLayer(movedNode, layerNr);
movedNode.setX(newLoc.x);
graph.getLayer(layerNr).sortNodesByX();
graph.removeEmptyLayers();
graph.addEdges(movedNode, maxLayerLength);
}
}
/**
* Removes self-edges from nodes in the graph. If self-edges are to be included in the layout
* (`layoutSelfEdges` is true), it stores them in the node for later processing and marks the graph

View file

@ -169,6 +169,24 @@ public class LayoutEdge {
this.to = to;
}
/**
* Gets the absolute x-coordinate of the source node's connection point for this edge.
*
* @return The x-coordinate of the source node's connection point.
*/
public int getFromX() {
return from.getX() + getRelativeFromX();
}
/**
* Gets the absolute x-coordinate of the target node's connection point for this edge.
*
* @return The x-coordinate of the target node's connection point.
*/
public int getToX() {
return to.getX() + getRelativeToX();
}
/**
* Gets the relative horizontal position from the source node's left boundary to the edge's starting point.
*

View file

@ -23,6 +23,7 @@
*/
package com.sun.hotspot.igv.hierarchicallayout;
import static com.sun.hotspot.igv.hierarchicallayout.LayoutNode.NODE_POS_COMPARATOR;
import com.sun.hotspot.igv.layout.Link;
import com.sun.hotspot.igv.layout.Port;
import com.sun.hotspot.igv.layout.Vertex;
@ -52,6 +53,8 @@ public class LayoutGraph {
private final Set<Link> links;
private final SortedSet<Vertex> vertices;
private final LinkedHashMap<Vertex, Set<Port>> inputPorts;
private final LinkedHashMap<Vertex, Set<Port>> outputPorts;
private final LinkedHashMap<Port, Set<Link>> portLinks;
// Layout Management: LayoutNodes and LayoutLayers
private final LinkedHashMap<Vertex, LayoutNode> layoutNodes;
@ -69,9 +72,9 @@ public class LayoutGraph {
public LayoutGraph(Collection<? extends Link> links, Collection<? extends Vertex> additionalVertices) {
this.links = new HashSet<>(links);
vertices = new TreeSet<>(additionalVertices);
LinkedHashMap<Port, Set<Link>> portLinks = new LinkedHashMap<>(links.size());
portLinks = new LinkedHashMap<>(links.size());
inputPorts = new LinkedHashMap<>(links.size());
LinkedHashMap<Vertex, Set<Port>> outputPorts = new LinkedHashMap<>(links.size());
outputPorts = new LinkedHashMap<>(links.size());
for (Link link : links) {
assert link.getFrom() != null;
@ -175,6 +178,114 @@ public class LayoutGraph {
return Collections.unmodifiableList(allNodes);
}
/**
* Creates a new layer at the specified index in the layers list.
* Adjusts the layer numbers of existing nodes in layers below the inserted layer.
*
* @param layerNr The index at which to insert the new layer.
* @return The newly created LayoutLayer.
*/
private LayoutLayer createNewLayer(int layerNr) {
LayoutLayer layer = new LayoutLayer();
layers.add(layerNr, layer);
// update layer field in nodes below layerNr
for (int l = layerNr + 1; l < getLayerCount(); l++) {
for (LayoutNode layoutNode : getLayer(l)) {
layoutNode.setLayer(l);
}
}
return layer;
}
/**
* Deletes the layer at the specified index.
* Adjusts the layer numbers of existing nodes in layers below the deleted layer.
*
* @param layerNr The index of the layer to delete.
*/
private void deleteLayer(int layerNr) {
layers.remove(layerNr);
// Update the layer field in nodes below the deleted layer
for (int l = layerNr; l < getLayerCount(); l++) {
for (LayoutNode layoutNode : getLayer(l)) {
layoutNode.setLayer(l);
}
}
}
/**
* Ensures that no neighboring nodes of the specified node are in the same layer.
* If any neighbor is found in the specified layer, inserts a new layer to avoid conflicts.
* Returns the adjusted layer number where the node can be safely inserted.
*
* @param node The LayoutNode to check and possibly reposition.
* @param layerNr The proposed layer number for the node.
* @return The layer number where the node can be safely inserted after adjustments.
*/
public int insertNewLayerIfNeeded(LayoutNode node, int layerNr) {
for (Link inputLink : getInputLinks(node.getVertex())) {
if (inputLink.getFrom().getVertex() == inputLink.getTo().getVertex()) continue;
LayoutNode fromNode = getLayoutNode(inputLink.getFrom().getVertex());
if (fromNode.getLayer() == layerNr) {
moveExpandLayerDown(layerNr + 1);
return layerNr + 1;
}
}
for (Link outputLink : getOutputLinks(node.getVertex())) {
if (outputLink.getFrom().getVertex() == outputLink.getTo().getVertex()) continue;
LayoutNode toNode = getLayoutNode(outputLink.getTo().getVertex());
if (toNode.getLayer() == layerNr) {
moveExpandLayerDown(layerNr);
return layerNr;
}
}
return layerNr;
}
/**
* Inserts a new layer at the specified index and adjusts nodes and edges accordingly.
* Moves existing nodes and their successors down to accommodate the new layer.
*
* @param layerNr The index at which to insert the new layer.
*/
private void moveExpandLayerDown(int layerNr) {
LayoutLayer newLayer = createNewLayer(layerNr);
if (layerNr == 0) return;
LayoutLayer layerAbove = getLayer(layerNr - 1);
for (LayoutNode fromNode : layerAbove) {
int fromX = fromNode.getX();
Map<Integer, List<LayoutEdge>> successorsByX = fromNode.groupSuccessorsByX();
fromNode.clearSuccessors();
for (Map.Entry<Integer, List<LayoutEdge>> entry : successorsByX.entrySet()) {
Integer relativeFromX = entry.getKey();
List<LayoutEdge> edges = entry.getValue();
LayoutNode dummyNode = new LayoutNode();
dummyNode.setX(fromX + relativeFromX);
dummyNode.setLayer(layerNr);
for (LayoutEdge edge : edges) {
dummyNode.addSuccessor(edge);
}
LayoutEdge dummyEdge = new LayoutEdge(fromNode, dummyNode, relativeFromX, 0, edges.get(0).getLink());
if (edges.get(0).isReversed()) dummyEdge.reverse();
fromNode.addSuccessor(dummyEdge);
dummyNode.addPredecessor(dummyEdge);
for (LayoutEdge edge : edges) {
edge.setFrom(dummyNode);
}
addDummyToLayer(dummyNode, layerNr);
}
}
newLayer.sortNodesByX();
}
/**
* Retrieves an unmodifiable list of all layers in the graph.
*
@ -193,6 +304,31 @@ public class LayoutGraph {
return layers.size();
}
/**
* Retrieves the LayoutNode associated with the specified Vertex.
*
* @param vertex The vertex whose LayoutNode is to be retrieved.
* @return The LayoutNode corresponding to the given vertex, or null if not found.
*/
public LayoutNode getLayoutNode(Vertex vertex) {
return layoutNodes.get(vertex);
}
/**
* Adds a LayoutNode to the specified layer and registers it in the graph.
*
* @param node The LayoutNode to add to the layer.
* @param layerNumber The index of the layer to which the node will be added.
*/
public void addNodeToLayer(LayoutNode node, int layerNumber) {
assert !node.isDummy();
node.setLayer(layerNumber);
getLayer(layerNumber).add(node);
if (!layoutNodes.containsKey(node.getVertex())) {
layoutNodes.put(node.getVertex(), node);
}
}
/**
* Adds a LayoutNode to the specified layer and registers it in the graph.
*
@ -283,6 +419,148 @@ public class LayoutGraph {
.collect(Collectors.toSet());
}
/**
* Retrieves all incoming links to the specified vertex.
*
* @param vertex The vertex whose incoming links are to be retrieved.
* @return A set of links that are incoming to the vertex.
*/
public List<Link> getInputLinks(Vertex vertex) {
List<Link> inputLinks = new ArrayList<>();
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
inputLinks.addAll(portLinks.getOrDefault(inputPort, Collections.emptySet()));
}
return inputLinks;
}
/**
* Retrieves all outgoing links from the specified vertex.
*
* @param vertex The vertex whose outgoing links are to be retrieved.
* @return A set of links that are outgoing from the vertex.
*/
public List<Link> getOutputLinks(Vertex vertex) {
List<Link> outputLinks = new ArrayList<>();
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
outputLinks.addAll(portLinks.getOrDefault(outputPort, Collections.emptySet()));
}
return outputLinks;
}
public List<Link> getAllLinks(Vertex vertex) {
List<Link> allLinks = new ArrayList<>();
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
allLinks.addAll(portLinks.getOrDefault(inputPort, Collections.emptySet()));
}
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
allLinks.addAll(portLinks.getOrDefault(outputPort, Collections.emptySet()));
}
return allLinks;
}
/**
* Removes the specified LayoutNode and all its connected edges from the graph.
*
* @param node The LayoutNode to remove along with its edges.
*/
public void removeNodeAndEdges(LayoutNode node) {
assert !node.isDummy();
removeEdges(node); // a node can only be removed together with its edges
int layer = node.getLayer();
layers.get(layer).remove(node);
layers.get(layer).updateNodeIndices();
layoutNodes.remove(node.getVertex());
}
/**
* Removes all edges connected to the specified LayoutNode.
* Handles the removal of associated dummy nodes if they are no longer needed.
* Updates the graph structure accordingly after node movement.
*
* @param node The LayoutNode whose connected edges are to be removed.
*/
public void removeEdges(LayoutNode node) {
assert !node.isDummy();
for (Link link : getAllLinks(node.getVertex())) {
removeEdge(link);
}
}
public void removeEdge(Link link) {
Vertex from = link.getFrom().getVertex();
Vertex to = link.getTo().getVertex();
LayoutNode toNode = getLayoutNode(to);
LayoutNode fromNode = getLayoutNode(from);
if (toNode.getLayer() < fromNode.getLayer()) {
// Reversed edge
toNode = fromNode;
}
// Remove preds-edges bottom up, starting at "to" node
// Cannot start from "from" node since there might be joint edges
List<LayoutEdge> toNodePredsEdges = List.copyOf(toNode.getPredecessors());
for (LayoutEdge edge : toNodePredsEdges) {
LayoutNode predNode = edge.getFrom();
LayoutEdge edgeToRemove;
if (edge.getLink() != null && edge.getLink().equals(link)) {
toNode.removePredecessor(edge);
edgeToRemove = edge;
} else {
// Wrong edge, look at next
continue;
}
if (!predNode.isDummy() && predNode.getVertex().equals(from)) {
// No dummy nodes inbetween 'from' and 'to' vertex
predNode.removeSuccessor(edgeToRemove);
break;
} else {
// Must remove edges between dummy nodes
boolean found = true;
LayoutNode succNode = toNode;
while (predNode.isDummy() && found) {
found = false;
if (predNode.getSuccessors().size() <= 1 && predNode.getPredecessors().size() <= 1) {
// Dummy node used only for this link, remove if not already removed
assert predNode.isDummy();
int layer = predNode.getLayer();
layers.get(layer).remove(predNode);
layers.get(layer).updateNodeIndices();
dummyNodes.remove(predNode);
} else {
// anchor node, should not be removed
break;
}
if (predNode.getPredecessors().size() == 1) {
predNode.removeSuccessor(edgeToRemove);
succNode = predNode;
edgeToRemove = predNode.getPredecessors().get(0);
predNode = edgeToRemove.getFrom();
found = true;
}
}
predNode.removeSuccessor(edgeToRemove);
succNode.removePredecessor(edgeToRemove);
}
break;
}
if (fromNode.getReversedLinkStartPoints().containsKey(link)) {
fromNode.computeReversedLinkPoints(false);
}
if (toNode.getReversedLinkStartPoints().containsKey(link)) {
toNode.computeReversedLinkPoints(false);
}
}
/**
* Retrieves the LayoutLayer at the specified index.
*
@ -293,6 +571,30 @@ public class LayoutGraph {
return layers.get(layerNr);
}
/**
* Finds the layer closest to the given y-coordinate.
*
* @param y the y-coordinate to check
* @return the index of the optimal layer, or -1 if no layers are found
*/
public int findLayer(int y) {
int optimalLayer = -1;
int minDistance = Integer.MAX_VALUE;
for (int l = 0; l < getLayerCount(); l++) {
// Check if y is within this layer's bounds
if (y >= getLayer(l).getTop() && y <= getLayer(l).getBottom()) {
return l;
}
int distance = Math.abs(getLayer(l).getCenter() - y);
if (distance < minDistance) {
minDistance = distance;
optimalLayer = l;
}
}
return optimalLayer;
}
/**
* Positions the layers vertically, calculating their heights and setting their positions.
* Centers the nodes within each layer vertically.
@ -314,6 +616,380 @@ public class LayoutGraph {
}
}
/**
* Optimizes routing of reversed (back) edges to reduce crossings.
*/
public void optimizeBackEdgeCrossings() {
for (LayoutNode node : getLayoutNodes()) {
node.optimizeBackEdgeCrossing();
}
}
/**
* Removes empty layers from the graph.
* Iteratively checks for and removes layers that contain only dummy nodes.
*/
public void removeEmptyLayers() {
int i = 0;
while (i < getLayerCount()) {
LayoutLayer layer = getLayer(i);
if (layer.containsOnlyDummyNodes()) {
removeEmptyLayer(i);
} else {
i++; // Move to the next layer only if no removal occurred
}
}
}
/**
* Removes the layer at the specified index if it is empty or contains only dummy nodes.
* Adjusts the positions of nodes and edges accordingly.
*
* @param layerNr The index of the layer to remove.
*/
private void removeEmptyLayer(int layerNr) {
LayoutLayer layer = getLayer(layerNr);
if (!layer.containsOnlyDummyNodes()) return;
for (LayoutNode dummyNode : layer) {
if (dummyNode.getSuccessors().isEmpty()) {
dummyNode.setLayer(layerNr + 1);
getLayer(layerNr + 1).add(dummyNode);
dummyNode.setX(dummyNode.calculateOptimalXFromPredecessors(true));
getLayer(layerNr + 1).sortNodesByX();
continue;
} else if (dummyNode.getPredecessors().isEmpty()) {
dummyNode.setLayer(layerNr - 1);
dummyNode.setX(dummyNode.calculateOptimalXFromSuccessors(true));
getLayer(layerNr - 1).add(dummyNode);
getLayer(layerNr - 1).sortNodesByX();
continue;
}
LayoutEdge layoutEdge = dummyNode.getPredecessors().get(0);
// remove the layoutEdge
LayoutNode fromNode = layoutEdge.getFrom();
fromNode.removeSuccessor(layoutEdge);
List<LayoutEdge> successorEdges = dummyNode.getSuccessors();
for (LayoutEdge successorEdge : successorEdges) {
successorEdge.setRelativeFromX(layoutEdge.getRelativeFromX());
successorEdge.setFrom(fromNode);
fromNode.addSuccessor(successorEdge);
}
dummyNode.clearPredecessors();
dummyNode.clearSuccessors();
dummyNodes.remove(dummyNode);
}
deleteLayer(layerNr);
}
/**
* Repositions the specified LayoutNode horizontally within its layer to the new x-coordinate.
* Ensures no overlap with adjacent nodes and maintains minimum spacing.
*
* @param layoutNode The LayoutNode to reposition.
* @param newX The new x-coordinate to set for the node.
*/
private void repositionLayoutNodeX(LayoutNode layoutNode, int newX) {
int currentX = layoutNode.getX();
// Early exit if the desired position is the same as the current position
if (newX == currentX) {
return;
}
LayoutLayer layer = getLayer(layoutNode.getLayer());
if (newX > currentX) {
layer.tryShiftNodeRight(layoutNode, newX);
} else {
layer.tryShiftNodeLeft(layoutNode, newX);
}
}
/**
* Aligns the x-coordinate of a single dummy successor node for the given LayoutNode.
* If the node has exactly one successor and that successor is a dummy node,
* sets the dummy node's x-coordinate to align with the current node or the edge's starting point.
*
* @param node The LayoutNode whose dummy successor is to be aligned.
*/
private void alignSingleSuccessorDummyNodeX(LayoutNode node) {
// Retrieve the list of successor edges
List<LayoutEdge> successors = node.getSuccessors();
// Proceed only if there is exactly one successor
if (successors.size() != 1) {
return;
}
LayoutEdge successorEdge = successors.get(0);
LayoutNode successorNode = successorEdge.getTo();
// Proceed only if the successor node is a dummy node
if (!successorNode.isDummy()) {
return;
}
// Determine the target x-coordinate based on whether the current node is a dummy
int targetX = node.isDummy() ? node.getX() : successorEdge.getStartX();
// Align the successor dummy node to the target x-coordinate
repositionLayoutNodeX(successorNode, targetX);
}
/**
* Aligns the x-coordinates of dummy successor nodes within the specified layer.
* Performs alignment in both forward and backward directions to ensure consistency.
*
* @param layer The LayoutLayer whose nodes' dummy successors need alignment.
*/
private void alignLayerDummySuccessors(LayoutLayer layer) {
// Forward pass: Align dummy successors from the first node to the last.
for (LayoutNode node : layer) {
alignSingleSuccessorDummyNodeX(node);
}
// Backward pass: Align dummy successors from the last node to the first.
for (int i = layer.size() - 1; i >= 0; i--) {
LayoutNode node = layer.get(i);
alignSingleSuccessorDummyNodeX(node);
}
}
/**
* Straightens edges in the graph by aligning dummy nodes to reduce bends.
* Processes all layers to align dummy successor nodes.
*/
public void straightenEdges() {
// Forward pass: Align dummy successors from the first layer to the last.
for (int i = 0; i < getLayerCount(); i++) {
alignLayerDummySuccessors(getLayer(i));
}
// Backward pass: Align dummy successors from the last layer to the first.
for (int i = getLayerCount() - 1; i >= 0; i--) {
alignLayerDummySuccessors(getLayer(i));
}
}
/**
* Updates the minimum X spacing for all layers in the graph.
*/
public void updateLayerMinXSpacing() {
for (LayoutLayer layer : this.getLayers()) {
layer.updateMinXSpacing(false);
}
}
/**
* Calculates the optimal horizontal position (index) for the specified node within the given layer,
* aiming to minimize the number of edge crossings.
*
* @param node The node to position.
* @param layerNr The index of the layer in which to position the node.
* @return The optimal position index within the layer for the node.
*/
private int optimalPosition(LayoutNode node, int layerNr) {
getLayer(layerNr).sort(NODE_POS_COMPARATOR);
int edgeCrossings = Integer.MAX_VALUE;
int optimalPos = -1;
// Try each possible position in the layerNr
for (int i = 0; i < getLayer(layerNr).size() + 1; i++) {
int xCoord;
if (i == 0) {
xCoord = getLayer(layerNr).get(i).getX() - node.getWidth() - 1;
} else {
xCoord = getLayer(layerNr).get(i - 1).getX() + getLayer(layerNr).get(i - 1).getWidth() + 1;
}
int currentCrossings = 0;
if (0 <= layerNr - 1) {
// For each link with an end point in vertex, check how many edges cross it
for (LayoutEdge edge : node.getPredecessors()) {
if (edge.getFrom().getLayer() == layerNr - 1) {
int fromNodeXCoord = edge.getFromX();
int toNodeXCoord = xCoord;
if (!node.isDummy()) {
toNodeXCoord += edge.getRelativeToX();
}
for (LayoutNode n : getLayer(layerNr - 1)) {
for (LayoutEdge e : n.getSuccessors()) {
if (e.getTo() == null) {
continue;
}
int compFromXCoord = e.getFromX();
int compToXCoord = e.getToX();
if ((fromNodeXCoord > compFromXCoord && toNodeXCoord < compToXCoord)
|| (fromNodeXCoord < compFromXCoord
&& toNodeXCoord > compToXCoord)) {
currentCrossings += 1;
}
}
}
}
}
}
// Edge crossings across current layerNr and layerNr below
if (layerNr + 1 < getLayerCount()) {
// For each link with an end point in vertex, check how many edges cross it
for (LayoutEdge edge : node.getSuccessors()) {
if (edge.getTo().getLayer() == layerNr + 1) {
int toNodeXCoord = edge.getToX();
int fromNodeXCoord = xCoord;
if (!node.isDummy()) {
fromNodeXCoord += edge.getRelativeFromX();
}
for (LayoutNode n : getLayer(layerNr + 1)) {
for (LayoutEdge e : n.getPredecessors()) {
if (e.getFrom() == null) {
continue;
}
int compFromXCoord = e.getFromX();
int compToXCoord = e.getToX();
if ((fromNodeXCoord > compFromXCoord && toNodeXCoord < compToXCoord)
|| (fromNodeXCoord < compFromXCoord
&& toNodeXCoord > compToXCoord)) {
currentCrossings += 1;
}
}
}
}
}
}
if (currentCrossings <= edgeCrossings) {
edgeCrossings = currentCrossings;
optimalPos = i;
}
}
return optimalPos;
}
/**
* Creates layout edges for the specified node and reverses edges as needed.
* Reverses edges that go from lower to higher layers to maintain proper layering.
*
* @param node The LayoutNode for which to create and reverse edges.
*/
public void createAndReverseLayoutEdges(LayoutNode node) {
List<Link> nodeLinks = new ArrayList<>(getInputLinks(node.getVertex()));
nodeLinks.addAll(getOutputLinks(node.getVertex()));
nodeLinks.sort(LINK_COMPARATOR);
List<LayoutNode> reversedLayoutNodes = new ArrayList<>();
for (Link link : nodeLinks) {
if (link.getFrom().getVertex() == link.getTo().getVertex()) continue;
LayoutEdge layoutEdge = createLayoutEdge(link);
LayoutNode fromNode = layoutEdge.getFrom();
LayoutNode toNode = layoutEdge.getTo();
if (fromNode.getLayer() > toNode.getLayer()) {
HierarchicalLayoutManager.ReverseEdges.reverseEdge(layoutEdge);
reversedLayoutNodes.add(fromNode);
reversedLayoutNodes.add(toNode);
}
}
// ReverseEdges
for (LayoutNode layoutNode : reversedLayoutNodes) {
layoutNode.computeReversedLinkPoints(false);
}
}
/**
* Inserts dummy nodes along the edges from predecessors of the specified node,
* for edges that span more than one layer.
*
* @param layoutNode The node for which to create predecessor dummy nodes.
*/
public void createDummiesForNodePredecessor(LayoutNode layoutNode) {
for (LayoutEdge predEdge : layoutNode.getPredecessors()) {
LayoutNode fromNode = predEdge.getFrom();
LayoutNode toNode = predEdge.getTo();
if (Math.abs(toNode.getLayer() - fromNode.getLayer()) <= 1) continue;
boolean hasEdgeFromSamePort = false;
LayoutEdge edgeFromSamePort = new LayoutEdge(fromNode, toNode, predEdge.getLink());
if (predEdge.isReversed()) edgeFromSamePort.reverse();
for (LayoutEdge succEdge : fromNode.getSuccessors()) {
if (succEdge.getRelativeFromX() == predEdge.getRelativeFromX() && succEdge.getTo().isDummy()) {
edgeFromSamePort = succEdge;
hasEdgeFromSamePort = true;
break;
}
}
if (hasEdgeFromSamePort) {
LayoutEdge curEdge = edgeFromSamePort;
boolean newEdge = true;
while (curEdge.getTo().getLayer() < toNode.getLayer() - 1 && curEdge.getTo().isDummy() && newEdge) {
// Traverse down the chain of dummy nodes linking together the edges originating
// from the same port
newEdge = false;
if (curEdge.getTo().getSuccessors().size() == 1) {
curEdge = curEdge.getTo().getSuccessors().get(0);
newEdge = true;
} else {
for (LayoutEdge e : curEdge.getTo().getSuccessors()) {
if (e.getTo().isDummy()) {
curEdge = e;
newEdge = true;
break;
}
}
}
}
LayoutNode prevDummy;
if (!curEdge.getTo().isDummy()) {
prevDummy = curEdge.getFrom();
} else {
prevDummy = curEdge.getTo();
}
predEdge.setFrom(prevDummy);
predEdge.setRelativeFromX(prevDummy.getWidth() / 2);
fromNode.removeSuccessor(predEdge);
prevDummy.addSuccessor(predEdge);
}
LayoutNode layoutNode1 = predEdge.getTo();
if (predEdge.getTo().getLayer() - 1 > predEdge.getFrom().getLayer()) {
LayoutEdge prevEdge = predEdge;
for (int l = layoutNode1.getLayer() - 1; l > prevEdge.getFrom().getLayer(); l--) {
LayoutNode dummyNode = new LayoutNode();
dummyNode.addSuccessor(prevEdge);
LayoutEdge result = new LayoutEdge(prevEdge.getFrom(), dummyNode, prevEdge.getRelativeFromX(), 0, prevEdge.getLink());
if (prevEdge.isReversed()) result.reverse();
dummyNode.addPredecessor(result);
prevEdge.setRelativeFromX(0);
prevEdge.getFrom().removeSuccessor(prevEdge);
prevEdge.getFrom().addSuccessor(result);
prevEdge.setFrom(dummyNode);
dummyNode.setLayer(l);
List<LayoutNode> layerNodes = getLayer(l);
if (layerNodes.isEmpty()) {
dummyNode.setPos(0);
} else {
dummyNode.setPos(optimalPosition(dummyNode, l));
}
for (LayoutNode n : layerNodes) {
if (n.getPos() >= dummyNode.getPos()) {
n.setPos(n.getPos() + 1);
}
}
addDummyToLayer(dummyNode, l);
prevEdge = dummyNode.getPredecessors().get(0);
}
}
}
}
/**
* Inserts dummy nodes along the edges to successors of the specified node,
* for edges that span more than one layer.
@ -430,4 +1106,20 @@ public class LayoutGraph {
}
}
}
/**
* Adds edges connected to the specified node, including any necessary dummy nodes.
* Handles edge reversal, dummy node insertion for both predecessors and successors,
* and updates node positions accordingly.
*
* @param node The LayoutNode to which edges will be added.
* @param maxLayerLength The maximum number of layers an edge can span without splitting it
*/
public void addEdges(LayoutNode node, int maxLayerLength) {
assert !node.isDummy();
createAndReverseLayoutEdges(node);
createDummiesForNodeSuccessor(node, maxLayerLength);
createDummiesForNodePredecessor(node);
updatePositions();
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -154,6 +154,10 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
y = top;
}
public int getCenter() {
return y + height / 2;
}
/**
* Gets the bottom Y-coordinate of this layer.
*
@ -181,6 +185,53 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
this.height = height;
}
/**
* Checks if this layer contains only dummy nodes.
*
* @return true if all nodes in the layer are dummy nodes; false otherwise.
*/
public boolean containsOnlyDummyNodes() {
for (LayoutNode node : this) {
if (!node.isDummy()) {
return false;
}
}
return true;
}
/**
* Sorts the nodes in this layer by their X-coordinate in increasing order.
* Assigns position indices to nodes based on the sorted order.
* Adjusts the X-coordinates of nodes to ensure minimum spacing between them.
*/
public void sortNodesByX() {
if (isEmpty()) return;
sort(NODE_X_COMPARATOR); // Sort nodes in the layer increasingly by x
updateNodeIndices();
updateMinXSpacing(false);
}
/**
* Ensures nodes have minimum horizontal spacing by adjusting their X positions.
*
* @param startFromZero if true, starts positioning from X = 0; otherwise, uses the first node's current X.
*/
public void updateMinXSpacing(boolean startFromZero) {
if (isEmpty()) {
return; // No nodes to adjust.
}
int minX = startFromZero ? 0 : this.get(0).getX();
for (LayoutNode node : this) {
int x = Math.max(node.getX(), minX);
node.setX(x);
minX = x + node.getOuterWidth() + NODE_OFFSET;
}
}
/**
* Initializes nodes' X positions with spacing.
*/
@ -203,4 +254,60 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
pos++;
}
}
/**
* Attempts to move the specified node to the right within the layer to the given X-coordinate.
* Ensures that the node does not overlap with its right neighbor by checking required spacing.
* If movement is possible without causing overlap, the node's X-coordinate is updated.
*
* @param layoutNode The node to move.
* @param newX The desired new X-coordinate for the node.
*/
public void tryShiftNodeRight(LayoutNode layoutNode, int newX) {
int currentX = layoutNode.getX();
int shiftAmount = newX - currentX;
int rightPos = layoutNode.getPos() + 1;
if (rightPos < size()) {
// There is a right neighbor
LayoutNode rightNeighbor = get(rightPos);
int proposedRightEdge = layoutNode.getRight() + shiftAmount;
int requiredLeftEdge = rightNeighbor.getOuterLeft() - NODE_OFFSET;
if (proposedRightEdge <= requiredLeftEdge) {
layoutNode.setX(newX);
}
} else {
// No right neighbor; safe to move freely to the right
layoutNode.setX(newX);
}
}
/**
* Attempts to move the specified node to the left within the layer to the given X-coordinate.
* Ensures that the node does not overlap with its left neighbor by checking required spacing.
* If movement is possible without causing overlap, the node's X-coordinate is updated.
*
* @param layoutNode The node to move.
* @param newX The desired new X-coordinate for the node.
*/
public void tryShiftNodeLeft(LayoutNode layoutNode, int newX) {
int currentX = layoutNode.getX();
int shiftAmount = currentX - newX;
int leftPos = layoutNode.getPos() - 1;
if (leftPos >= 0) {
// There is a left neighbor
LayoutNode leftNeighbor = get(leftPos);
int proposedLeftEdge = layoutNode.getLeft() - shiftAmount;
int requiredRightEdge = leftNeighbor.getOuterRight() + NODE_OFFSET;
if (requiredRightEdge <= proposedLeftEdge) {
layoutNode.setX(newX);
}
} else {
// No left neighbor; safe to move freely to the left
layoutNode.setX(newX);
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
package com.sun.hotspot.igv.hierarchicallayout;
import com.sun.hotspot.igv.layout.Vertex;
import java.awt.Point;
import java.util.Set;
public interface LayoutMover {
/**
* Moves a link by shifting its position along the X-axis.
*
* @param linkPos The current position of the link.
* @param shiftX The amount to shift the link along the X-axis.
*/
void moveLink(Point linkPos, int shiftX);
/**
* Moves a set of vertices.
*
* @param movedVertices A set of vertices to be moved.
*/
void moveVertices(Set<? extends Vertex> movedVertices);
/**
* Moves a single vertex.
*
* @param movedVertex The vertex to be moved.
*/
void moveVertex(Vertex movedVertex);
}

View file

@ -69,6 +69,7 @@ public class LayoutNode {
private int rightMargin;
private int leftMargin;
private int pos = -1; // Position within its layer
private boolean reverseLeft = false;
private int crossingNumber = 0;
public boolean hasSelfEdge() {
@ -250,6 +251,15 @@ public class LayoutNode {
return x + leftMargin;
}
/**
* Gets the outer left boundary (including left margin) of the node.
*
* @return The x-coordinate of the outer left boundary.
*/
public int getOuterLeft() {
return x;
}
/**
* Gets the total width of the node, including left and right margins.
*
@ -272,6 +282,15 @@ public class LayoutNode {
return height;
}
/**
* Gets the right boundary (excluding right margin) of the node.
*
* @return The x-coordinate of the right boundary.
*/
public int getRight() {
return x + leftMargin + width;
}
/**
* Gets the outer right boundary (including right margin) of the node.
*
@ -436,6 +455,50 @@ public class LayoutNode {
this.pos = pos;
}
/**
* Groups the successor edges by their relative x-coordinate from the current node.
*
* @return A map of relative x-coordinate to list of successor edges.
*/
public Map<Integer, List<LayoutEdge>> groupSuccessorsByX() {
Map<Integer, List<LayoutEdge>> result = new HashMap<>();
for (LayoutEdge succEdge : succs) {
result.computeIfAbsent(succEdge.getRelativeFromX(), k -> new ArrayList<>()).add(succEdge);
}
return result;
}
private int getBackedgeCrossingScore() {
int score = 0;
for (LayoutEdge predEdge : preds) {
if (predEdge.isReversed()) {
List<Point> points = reversedLinkEndPoints.get(predEdge.getLink());
if (points != null) {
int x0 = points.get(points.size() - 1).x;
int xn = points.get(0).x;
int startPoint = predEdge.getStartX();
int endPoint = predEdge.getEndX();
int win = (x0 < xn) ? (startPoint - endPoint) : (endPoint - startPoint);
score += win;
}
}
}
for (LayoutEdge succEdge : succs) {
if (succEdge.isReversed()) {
List<Point> points = reversedLinkStartPoints.get(succEdge.getLink());
if (points != null) {
int x0 = points.get(points.size() - 1).x;
int xn = points.get(0).x;
int startPoint = succEdge.getStartX();
int endPoint = succEdge.getEndX();
int win = (x0 > xn) ? (startPoint - endPoint) : (endPoint - startPoint);
score += win;
}
}
}
return score;
}
private boolean computeReversedStartPoints(boolean left) {
TreeMap<Integer, ArrayList<LayoutEdge>> sortedDownMap = left ? new TreeMap<>() : new TreeMap<>(Collections.reverseOrder());
for (LayoutEdge succEdge : succs) {
@ -518,12 +581,28 @@ public class LayoutNode {
}
public void computeReversedLinkPoints(boolean reverseLeft) {
this.reverseLeft = reverseLeft;
initSize();
reversedLinkStartPoints.clear();
reversedLinkEndPoints.clear();
boolean hasReversedDown = computeReversedStartPoints(reverseLeft);
computeReversedEndPoints(hasReversedDown != reverseLeft);
boolean hasReversedUP = computeReversedEndPoints(hasReversedDown != reverseLeft);
}
public boolean isReverseRight() {
return !reverseLeft;
}
public void optimizeBackEdgeCrossing() {
if (reversedLinkStartPoints.isEmpty() && reversedLinkEndPoints.isEmpty()) return;
int orig_score = getBackedgeCrossingScore();
computeReversedLinkPoints(isReverseRight());
int reverse_score = getBackedgeCrossingScore();
if (orig_score > reverse_score) {
computeReversedLinkPoints(isReverseRight());
}
}
public ArrayList<Point> getSelfEdgePoints() {

View file

@ -81,13 +81,17 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
private final LayerWidget mainLayer;
private final LayerWidget blockLayer;
private final LayerWidget connectionLayer;
private final Widget shadowWidget;
private final Widget pointerWidget;
private final DiagramViewModel model;
private ModelState modelState;
private boolean rebuilding;
private final Map<OutputSlot, Set<LineWidget>> outputSlotToLineWidget = new HashMap<>();
private final Map<InputSlot, Set<LineWidget>> inputSlotToLineWidget = new HashMap<>();
private final HierarchicalStableLayoutManager hierarchicalStableLayoutManager;
private HierarchicalLayoutManager seaLayoutManager;
private LayoutMover layoutMover;
/**
* The alpha level of partially visible figures.
@ -341,6 +345,12 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
mainLayer.setBorder(emptyBorder);
addChild(mainLayer);
pointerWidget = new Widget(DiagramScene.this);
addChild(pointerWidget);
shadowWidget = new Widget(DiagramScene.this);
addChild(shadowWidget);
setLayout(LayoutFactory.createAbsoluteLayout());
getActions().addAction(mouseZoomAction);
getActions().addAction(ActionFactory.createPopupMenuAction((widget, localLocation) -> createPopupMenu()));
@ -596,6 +606,163 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
}
private MoveProvider getFigureMoveProvider() {
return new MoveProvider() {
private boolean hasMoved = false; // Flag to track movement
private int startLayerY;
private void setFigureShadow(Figure f) {
FigureWidget fw = getWidget(f);
Color c = f.getColor();
Border border = new FigureWidget.RoundedBorder(new Color(0,0,0, 50), 1);
shadowWidget.setBorder(border);
shadowWidget.setBackground(new Color(c.getRed(), c.getGreen(), c.getBlue(), 50));
shadowWidget.setPreferredLocation(fw.getPreferredLocation());
shadowWidget.setPreferredSize(f.getSize());
shadowWidget.setVisible(true);
shadowWidget.setOpaque(true);
shadowWidget.revalidate();
shadowWidget.repaint();
}
private void setMovePointer(Figure f) {
Border border = new FigureWidget.RoundedBorder(Color.RED, 1);
pointerWidget.setBorder(border);
pointerWidget.setBackground(Color.RED);
pointerWidget.setPreferredBounds(new Rectangle(0, 0, 3, f.getSize().height));
pointerWidget.setVisible(false);
pointerWidget.setOpaque(true);
}
@Override
public void movementStarted(Widget widget) {
if (layoutMover == null) return; // Do nothing if layoutMover is not available
widget.bringToFront();
startLayerY = widget.getLocation().y;
hasMoved = false; // Reset the movement flag
Set<Figure> selectedFigures = model.getSelectedFigures();
if (selectedFigures.size() == 1) {
Figure selectedFigure = selectedFigures.iterator().next();
setFigureShadow(selectedFigure);
setMovePointer(selectedFigure);
}
}
@Override
public void movementFinished(Widget widget) {
shadowWidget.setVisible(false);
pointerWidget.setVisible(false);
if (layoutMover == null || !hasMoved) return; // Do nothing if layoutMover is not available or no movement occurred
rebuilding = true;
Set<Figure> movedFigures = new HashSet<>(model.getSelectedFigures());
for (Figure figure : movedFigures) {
FigureWidget fw = getWidget(figure);
figure.setPosition(new Point(fw.getLocation().x, fw.getLocation().y));
}
layoutMover.moveVertices(movedFigures);
rebuildConnectionLayer();
for (FigureWidget fw : getVisibleFigureWidgets()) {
fw.updatePosition();
}
validateAll();
addUndo();
rebuilding = false;
}
private static final int MAGNET_SIZE = 5;
private int magnetToStartLayerY(Widget widget, Point location) {
int shiftY = location.y - widget.getLocation().y;
if (Math.abs(location.y - startLayerY) <= MAGNET_SIZE) {
if (Math.abs(widget.getLocation().y - startLayerY) > MAGNET_SIZE) {
shiftY = startLayerY - widget.getLocation().y;
} else {
shiftY = 0;
}
}
return shiftY;
}
@Override
public Point getOriginalLocation(Widget widget) {
if (layoutMover == null) return widget.getLocation(); // default behavior
return ActionFactory.createDefaultMoveProvider().getOriginalLocation(widget);
}
@Override
public void setNewLocation(Widget widget, Point location) {
if (layoutMover == null) return; // Do nothing if layoutMover is not available
hasMoved = true; // Mark that a movement occurred
int shiftX = location.x - widget.getLocation().x;
int shiftY = magnetToStartLayerY(widget, location);
List<Figure> selectedFigures = new ArrayList<>( model.getSelectedFigures());
selectedFigures.sort(Comparator.comparingInt(f -> f.getPosition().x));
for (Figure figure : selectedFigures) {
FigureWidget fw = getWidget(figure);
for (InputSlot inputSlot : figure.getInputSlots()) {
assert inputSlot != null;
if (inputSlotToLineWidget.containsKey(inputSlot)) {
for (LineWidget lw : inputSlotToLineWidget.get(inputSlot)) {
assert lw != null;
Point toPt = lw.getTo();
Point fromPt = lw.getFrom();
if (toPt != null && fromPt != null) {
int xTo = toPt.x + shiftX;
int yTo = toPt.y + shiftY;
lw.setTo(new Point(xTo, yTo));
lw.setFrom(new Point(fromPt.x + shiftX, fromPt.y));
LineWidget pred = lw.getPredecessor();
pred.setTo(new Point(pred.getTo().x + shiftX, pred.getTo().y));
pred.revalidate();
lw.revalidate();
}
}
}
}
for (OutputSlot outputSlot : figure.getOutputSlots()) {
assert outputSlot != null;
if (outputSlotToLineWidget.containsKey(outputSlot)) {
for (LineWidget lw : outputSlotToLineWidget.get(outputSlot)) {
assert lw != null;
Point fromPt = lw.getFrom();
Point toPt = lw.getTo();
if (toPt != null && fromPt != null) {
int xFrom = fromPt.x + shiftX;
int yFrom = fromPt.y + shiftY;
lw.setFrom(new Point(xFrom, yFrom));
lw.setTo(new Point(toPt.x + shiftX, toPt.y));
for (LineWidget succ : lw.getSuccessors()) {
succ.setFrom(new Point(succ.getFrom().x + shiftX, succ.getFrom().y));
succ.revalidate();
}
lw.revalidate();
}
}
}
}
Point newLocation = new Point(fw.getLocation().x + shiftX, fw.getLocation().y + shiftY);
ActionFactory.createDefaultMoveProvider().setNewLocation(fw, newLocation);
}
FigureWidget fw = getWidget(selectedFigures.iterator().next());
pointerWidget.setVisible(true);
Point newLocation = new Point(fw.getLocation().x + shiftX -3, fw.getLocation().y + shiftY);
ActionFactory.createDefaultMoveProvider().setNewLocation(pointerWidget, newLocation);
connectionLayer.revalidate();
connectionLayer.repaint();
}
};
}
private void rebuildMainLayer() {
mainLayer.removeChildren();
for (Figure figure : getModel().getDiagram().getFigures()) {
@ -604,6 +771,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
figureWidget.getActions().addAction(ActionFactory.createPopupMenuAction(figureWidget));
figureWidget.getActions().addAction(selectAction);
figureWidget.getActions().addAction(hoverAction);
figureWidget.getActions().addAction(ActionFactory.createMoveAction(null, getFigureMoveProvider()));
addObject(figure, figureWidget);
mainLayer.addChild(figureWidget);
@ -737,6 +905,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
private void doStableSeaLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
layoutMover = null;
boolean enable = model.getCutEdges();
boolean previous = hierarchicalStableLayoutManager.getCutEdges();
hierarchicalStableLayoutManager.setCutEdges(enable);
@ -749,17 +918,20 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
private void doSeaLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
seaLayoutManager = new HierarchicalLayoutManager();
layoutMover = seaLayoutManager;
seaLayoutManager.setCutEdges(model.getCutEdges());
seaLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
}
private void doClusteredLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
layoutMover = null;
HierarchicalClusterLayoutManager clusterLayoutManager = new HierarchicalClusterLayoutManager();
clusterLayoutManager.setCutEdges(model.getCutEdges());
clusterLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
}
private void doCFGLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
layoutMover = null;
HierarchicalCFGLayoutManager cfgLayoutManager = new HierarchicalCFGLayoutManager(getVisibleBlockConnections(), getVisibleBlocks());
cfgLayoutManager.setCutEdges(model.getCutEdges());
cfgLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
@ -777,6 +949,84 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
private final Point specialNullPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
private MoveProvider getFigureConnectionMoveProvider() {
return new MoveProvider() {
Point startLocation;
Point originalPosition;
@Override
public void movementStarted(Widget widget) {
if (layoutMover == null) return; // Do nothing if layoutMover is not available
LineWidget lw = (LineWidget) widget;
startLocation = lw.getClientAreaLocation();
originalPosition = lw.getFrom();
}
@Override
public void movementFinished(Widget widget) {
if (layoutMover == null) return; // Do nothing if layoutMover is not available
LineWidget lineWidget = (LineWidget) widget;
if (lineWidget.getPredecessor() == null) return;
if (lineWidget.getSuccessors().isEmpty()) return;
if (lineWidget.getFrom().x != lineWidget.getTo().x) return;
int shiftX = lineWidget.getClientAreaLocation().x - startLocation.x;
if (shiftX == 0) return;
rebuilding = true;
layoutMover.moveLink(originalPosition, shiftX);
rebuildConnectionLayer();
for (FigureWidget fw : getVisibleFigureWidgets()) {
fw.updatePosition();
}
validateAll();
addUndo();
rebuilding = false;
}
@Override
public Point getOriginalLocation(Widget widget) {
if (layoutMover == null) return widget.getLocation(); // default behavior
LineWidget lineWidget = (LineWidget) widget;
return lineWidget.getClientAreaLocation();
}
@Override
public void setNewLocation(Widget widget, Point location) {
if (layoutMover == null) return; // Do nothing if layoutMover is not available
LineWidget lineWidget = (LineWidget) widget;
if (lineWidget.getPredecessor() == null) return;
if (lineWidget.getSuccessors().isEmpty()) return;
if (lineWidget.getFrom().x != lineWidget.getTo().x) return;
int shiftX = location.x - lineWidget.getClientAreaLocation().x;
if (shiftX == 0) return;
Point oldFrom = lineWidget.getFrom();
Point newFrom = new Point(oldFrom.x + shiftX, oldFrom.y);
Point oldTo = lineWidget.getTo();
Point newTo = new Point(oldTo.x + shiftX, oldTo.y);
lineWidget.setTo(newTo);
lineWidget.setFrom(newFrom);
lineWidget.revalidate();
LineWidget predecessor = lineWidget.getPredecessor();
Point toPt = predecessor.getTo();
predecessor.setTo(new Point(toPt.x + shiftX, toPt.y));
predecessor.revalidate();
for (LineWidget successor : lineWidget.getSuccessors()) {
Point fromPt = successor.getFrom();
successor.setFrom(new Point(fromPt.x + shiftX, fromPt.y));
successor.revalidate();
}
}
};
}
private void processOutputSlot(OutputSlot outputSlot, List<FigureConnection> connections, int controlPointIndex, Point lastPoint, LineWidget predecessor) {
Map<Point, List<FigureConnection>> pointMap = new HashMap<>(connections.size());
@ -824,6 +1074,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
newPredecessor.setVisible(isVisible);
if (predecessor == null) {
assert outputSlot != null;
if (outputSlotToLineWidget.containsKey(outputSlot)) {
outputSlotToLineWidget.get(outputSlot).add(newPredecessor);
} else {
@ -834,6 +1085,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
newWidgets.add(newPredecessor);
addObject(new ConnectionSet(connectionList), newPredecessor);
newPredecessor.getActions().addAction(hoverAction);
newPredecessor.getActions().addAction(ActionFactory.createMoveAction(null, getFigureConnectionMoveProvider()));
}
processOutputSlot(outputSlot, connectionList, controlPointIndex + 1, currentPoint, newPredecessor);
@ -843,6 +1095,8 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
for (FigureConnection connection : connections) {
if (isVisibleFigureConnection(connection)) {
InputSlot inputSlot = connection.getInputSlot();
if (predecessor != null) {
assert inputSlot != null;
if (inputSlotToLineWidget.containsKey(inputSlot)) {
inputSlotToLineWidget.get(inputSlot).add(predecessor);
} else {
@ -852,6 +1106,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
}
}
}
private void processBlockConnection(BlockConnection blockConnection) {
boolean isDashed = blockConnection.getStyle() == Connection.ConnectionStyle.DASHED;
@ -1212,9 +1467,6 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
}
Map<OutputSlot, Set<LineWidget>> outputSlotToLineWidget = new HashMap<>();
Map<InputSlot, Set<LineWidget>> inputSlotToLineWidget = new HashMap<>();
public JPopupMenu createPopupMenu() {
JPopupMenu menu = new JPopupMenu();

View file

@ -185,6 +185,10 @@ public class FigureWidget extends Widget implements Properties.Provider, PopupMe
this.setToolTipText(PropertiesConverter.convertToHTML(f.getProperties()));
}
public void updatePosition() {
setPreferredLocation(figure.getPosition());
}
public int getFigureHeight() {
return middleWidget.getPreferredBounds().height;
}

View file

@ -60,8 +60,8 @@ public class LineWidget extends Widget implements PopupMenuProvider {
private final OutputSlot outputSlot;
private final DiagramScene scene;
private final List<? extends Connection> connections;
private final Point from;
private final Point to;
private Point from;
private Point to;
private Rectangle clientArea;
private final LineWidget predecessor;
private final List<LineWidget> successors;
@ -125,6 +125,10 @@ public class LineWidget extends Widget implements PopupMenuProvider {
}));
}
public Point getClientAreaLocation() {
return clientArea.getLocation();
}
private void computeClientArea() {
int minX = from.x;
int minY = from.y;
@ -158,6 +162,16 @@ public class LineWidget extends Widget implements PopupMenuProvider {
return sb.toString();
}
public void setFrom(Point from) {
this.from = from;
computeClientArea();
}
public void setTo(Point to) {
this.to= to;
computeClientArea();
}
public Point getFrom() {
return from;
}
@ -166,6 +180,14 @@ public class LineWidget extends Widget implements PopupMenuProvider {
return to;
}
public LineWidget getPredecessor() {
return predecessor;
}
public List<LineWidget> getSuccessors() {
return Collections.unmodifiableList(successors);
}
private void addSuccessor(LineWidget widget) {
this.successors.add(widget);
}