Issue
I have nodes that I can move, these nodes are placed on the Pane, which is on the ScrollPane.When I drag a node outside of the viewportBounds
of the scrollPane, the vvalue
should change so that the node is again within those bounds. To solve it I tried to use answers from this question.
My problem is that after the node is again within the boundaries of the viewportBounds
, the cursor moves relative to the node, if I want to continue moving the node outside the viewport, after several iterations the cursor will move so much that it will be outside the entire application window and will rest against the screen boundaries. How do I maintain position of cursor on the node?
If you want to test code, keep in mind that the restructuring of the viewport
boundaries occurs only when you move nodes along the Y axis.
public class NewFXMain extends Application {
@Override
public void start(Stage primaryStage) {
AnchorPane root = new AnchorPane();
ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000,5000);
Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
DraggableNode node = new DraggableNode(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node.setStyle(
"-fx-background-color: #334488; "
+ "-fx-text-fill: black; "
+ "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class DraggableNode extends Pane {
private double x = 0;
private double y = 0;
private double mousex = 0;
private double mousey = 0;
private Node view;
private boolean dragging = false;
private boolean moveToFront = true;
private void ensureVisible(ScrollPane scrollPane) {
Bounds viewport = scrollPane.getViewportBounds();
double contentHeight = scrollPane.getContent().localToScene(scrollPane.getContent().getBoundsInLocal()).getHeight();
double nodeMinY = this.localToScene(this.getBoundsInLocal()).getMinY();
double nodeMaxY = this.localToScene(this.getBoundsInLocal()).getCenterY();
double vValueDelta = 0;
double vValueCurrent = scrollPane.getVvalue();
if (nodeMaxY < 0) {
// currently located above (remember, top left is (0,0))
vValueDelta = (nodeMinY) / contentHeight;
System.out.println("FIRST CASE DELTA: " + vValueDelta);
} else if (nodeMinY > viewport.getHeight()) {
// currently located below
vValueDelta = ((nodeMinY) / contentHeight) / 5;
System.out.println("SECOND CASE DELTA: " + vValueDelta);
}
scrollPane.setVvalue(vValueCurrent + vValueDelta);
}
public DraggableNode(ScrollPane pane) {
init(pane);
}
private void init(ScrollPane scroll) {
onMousePressedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// record the current mouse X and Y position on Node
mousex = event.getSceneX();
mousey = event.getSceneY();
x = getLayoutX();
y = getLayoutY();
if (isMoveToFront()) {
toFront();
}
}
});
//Event Listener for MouseDragged
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// Get the exact moved X and Y
double offsetX = event.getSceneX() - mousex;
double offsetY = event.getSceneY() - mousey;
x += offsetX;
y += offsetY;
double scaledX = x;
double scaledY = y;
setLayoutX(scaledX);
setLayoutY(scaledY);
ensureVisible(scroll);
dragging = true;
// again set current Mouse x AND y position
mousex = event.getSceneX();
mousey = event.getSceneY();
event.consume();
}
});
onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
dragging = false;
}
});
}
public void setMoveToFront(boolean moveToFront) {
this.moveToFront = moveToFront;
}
public boolean isMoveToFront() {
return moveToFront;
}
}
Solution
To answer your question:
How do I maintain position of cursor on the node?
You need to use the AWT Robot class to move the cursor.
private void movePointer(Node node){
Bounds b = node.localToScreen(node.getLayoutBounds());
try {
final Robot robot = new Robot();
robot.mouseMove((int) (b.getMinX()+(b.getWidth()/2)), (int) (b.getMinY()+(b.getHeight()/2)));
} catch (final AWTException e) {
System.out.println("Unable to poistion the pointer");
}
}
The above method will position the cursor at the center of the provided node. You can call the above method after you set the vValue.
Having said that, from a user perspective, it is very odd behavior
- to let the node go beyond the viewport bounds and then jump back suddenly to make it visible
- to jump the cursor
So I tried to implement in a way where the dragged node will never go beyond the viewport bounds and the vValue/hValue are adjusted based on drag and maintain the node to be visible in viewport.
To address the other issue of cursor reaching the app/screen bounds, I implemented a way to automatically start the scroll while dragging: once the cursor goes beyond the viewport bounds.When the cursor is back in the viewport bounds, it just behaves as a normal drag operation.
Below is the quick demo of the code with the desired changes. You can fine tune as per your needs.
I added a background to the pane to showcase the drag in progress operation ;)
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.Pair;
public class NewFXMainV1 extends Application {
public static void main(final String[] args) {
launch(args);
}
@Override
public void start(final Stage primaryStage) {
final AnchorPane root = new AnchorPane();
// Adding a pattern to background to observe the automatic drag :)
String bg1 = "linear-gradient(from 0% 0% to 1% 0% , repeat, #DDDDDD 50% , transparent 22% )";
String bg2 = "linear-gradient(from 0% 0% to 0% 1% , repeat, transparent 50% , #DDDDDD 22% )";
root.setStyle("-fx-background-color:" + bg1 + ", " + bg2 + ";");
final ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000, 5000);
final Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
final DraggableNodeV1 node = new DraggableNodeV1(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node
.setStyle(
"-fx-background-color: #334488; " + "-fx-text-fill: black; " + "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
}
class DraggableNodeV1 extends Pane {
private static Duration d = Duration.millis(26);
private static double PX_JUMP = 10;
private static double AUTOMATIC_OFFSET = 15;
private double x = 0;
private double y = 0;
private double mouseX = 0;
private double mouseY = 0;
private Timeline xTimeline;
private Timeline yTimeline;
public DraggableNodeV1(final ScrollPane pane) {
init(pane);
}
private void ensureVisible(final ScrollPane scrollPane) {
final Pane dragNode = this;
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final Bounds contentBounds = scrollPane.getContent().getLayoutBounds();
final double layoutY = dragNode.getLayoutY();
final double layoutX = dragNode.getLayoutX();
final double visibleYInViewport = vpMinY + (vpHeight - dragNode.getHeight());
double totalHeightToScroll = contentBounds.getHeight() - vpHeight;
if (layoutY > visibleYInViewport) {
double heightToScroll = vpMinY + (layoutY - visibleYInViewport);
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
} else if (layoutY < vpMinY) {
double heightToScroll = layoutY;
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
}
final double visibleXInViewport = vpMinX + (vpWidth - dragNode.getWidth());
double totalWidthToScroll = contentBounds.getWidth() - vpWidth;
if (layoutX > visibleXInViewport) {
double widthToScroll = vpMinX + (layoutX - visibleXInViewport);
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
} else if (layoutX < vpMinX) {
double widthToScroll = layoutX;
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
}
}
private void init(final ScrollPane scroll) {
onMousePressedProperty().set(event -> {
mouseX = event.getScreenX();
mouseY = event.getScreenY();
x = getLayoutX();
y = getLayoutY();
});
// Event Listener for MouseDragged
onMouseDraggedProperty().set(event -> {
if (isMouseInDraggableViewPort(event, scroll)) {
dragTheNode(event, scroll);
clearTimelines();
} else {
Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> auto = isMouseInAutomaticDrag(event, scroll);
Pair<Boolean, Boolean> xAuto = auto.getKey();
if (xAuto.getKey() || xAuto.getValue()) {
if (xAuto.getKey()) { // towards left
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutX() > 0) {
setLayoutX(getLayoutX() - PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
} else if (xAuto.getValue()) { // towards right
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutX() < contentBounds.getWidth() - getWidth()) {
setLayoutX(getLayoutX() + PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
}
} else {
stopXTimeline();
}
Pair<Boolean, Boolean> yAuto = auto.getValue();
if (yAuto.getKey() || yAuto.getValue()) {
if (yAuto.getKey()) { // towards top
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutY() > 0) {
setLayoutY(getLayoutY() - PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
} else if (yAuto.getValue()) { // towards bottom
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutY() < contentBounds.getHeight() - getHeight()) {
setLayoutY(getLayoutY() + PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
}
} else {
stopYTimeline();
}
}
event.consume();
});
onMouseReleasedProperty().set(event -> clearTimelines());
}
private void clearTimelines() {
stopXTimeline();
stopYTimeline();
}
private void stopXTimeline() {
if (xTimeline != null) {
xTimeline.stop();
}
xTimeline = null;
}
private void stopYTimeline() {
if (yTimeline != null) {
yTimeline.stop();
}
yTimeline = null;
}
private Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> isMouseInAutomaticDrag(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
double eX = event.getSceneX();
double eY = event.getSceneY();
double vpMinX = viewportSceneBounds.getMinX();
double vpMaxX = viewportSceneBounds.getMaxX();
double vpMinY = viewportSceneBounds.getMinY();
double vpMaxY = viewportSceneBounds.getMaxY();
Pair<Boolean, Boolean> autoX = new Pair<>(eX <= vpMinX + AUTOMATIC_OFFSET, vpMaxX - AUTOMATIC_OFFSET <= eX);
Pair<Boolean, Boolean> autoY = new Pair<>(eY <= vpMinY + AUTOMATIC_OFFSET, vpMaxY - AUTOMATIC_OFFSET <= eY);
return new Pair<>(autoX, autoY);
}
private void dragTheNode(MouseEvent event, ScrollPane scroll) {
final double offsetX = event.getScreenX() - mouseX;
final double offsetY = event.getScreenY() - mouseY;
final double tX = x + offsetX;
final double tY = y + offsetY;
Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (tX >= 0 && tX <= (contentBounds.getWidth() - getWidth())) {
determineX(scroll, tX, event);
} else if (tX < 0) {
setLayoutX(0);
} else {
setLayoutX(contentBounds.getWidth() - getWidth());
}
if (tY >= 0 && tY <= (contentBounds.getHeight() - getHeight())) {
determineY(scroll, tY, event);
} else if (tY < 0) {
setLayoutY(0);
} else {
setLayoutY(contentBounds.getHeight() - getHeight());
}
ensureVisible(scroll);
}
private boolean isMouseInDraggableViewPort(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
Bounds draggableBounds = new BoundingBox(viewportSceneBounds.getMinX() + AUTOMATIC_OFFSET,
viewportSceneBounds.getMinY() + AUTOMATIC_OFFSET,
viewportSceneBounds.getWidth() - (2 * AUTOMATIC_OFFSET),
viewportSceneBounds.getHeight() - (2 * AUTOMATIC_OFFSET));
return draggableBounds.contains(event.getSceneX(), event.getSceneY());
}
private void determineY(ScrollPane scrollPane, double tY, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
final double visibleYInViewport = vpMinY + (vpHeight - getHeight());
if (tY >= vpMinY && tY <= visibleYInViewport) {
setLayoutY(tY);
} else if (tY < vpMinY) {
setLayoutY(vpMinY);
} else {
setLayoutY(visibleYInViewport);
}
}
private void determineX(ScrollPane scrollPane, double tX, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final double visibleXInViewport = vpMinX + (vpWidth - getHeight());
if (tX >= vpMinX && tX <= visibleXInViewport) {
setLayoutX(tX);
} else if (tX < vpMinX) {
setLayoutX(vpMinX);
} else {
setLayoutX(visibleXInViewport);
}
}
}
Answered By - Sai Dandem
Answer Checked By - Marilyn (JavaFixing Volunteer)