Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8087684

Drag detection fails when child Node under mouse pointer is removed from Scene

    Details

    • Type: Bug
    • Status: Closed
    • Priority: P4
    • Resolution: Cannot Reproduce
    • Affects Version/s: 8u40
    • Fix Version/s: 9
    • Component/s: javafx
    • Labels:

      Description

      I've attached a small program. It can be used to draw stuff on a grid. Left mouse button to place things, right mouse button to delete things. Dragging places/deletes multiple things. To reproduce:

      1) Run program
      2) Draw a few cells with left mouse button (dragging works, see output spam in console)
      3) Right click+hold on a filled cell without moving the mouse, wait half a second
      4) While keeping the right mouse button down try drag to remove more cells --> notice that no drag events come in

      I suspect this is caused by the fact that the node that was right clicked is a child node of the layout area (and its removed by the right click) -- the drag handler however is on the container, and removing the child (which never handled the right click event, it did not consume it) seems to break the drag detection on the parent.

      If you do the same thing, but start on an empty area in the grid, the dragging with right mouse button works and child nodes get deleted as the mouse passed over them.

      package hs.scl;

      import hs.scl.DraggableNode01.CityObject.Type;

      import java.io.BufferedReader;
      import java.io.BufferedWriter;
      import java.io.IOException;
      import java.nio.file.Files;
      import java.nio.file.Path;
      import java.nio.file.Paths;
      import java.nio.file.StandardCopyOption;
      import java.util.HashMap;
      import java.util.LinkedHashMap;
      import java.util.Map;

      import javafx.application.Application;
      import javafx.event.EventHandler;
      import javafx.geometry.Insets;
      import javafx.geometry.Point2D;
      import javafx.scene.Node;
      import javafx.scene.Scene;
      import javafx.scene.canvas.Canvas;
      import javafx.scene.canvas.GraphicsContext;
      import javafx.scene.control.Button;
      import javafx.scene.control.Label;
      import javafx.scene.control.ToggleButton;
      import javafx.scene.control.ToggleGroup;
      import javafx.scene.effect.DropShadow;
      import javafx.scene.input.MouseEvent;
      import javafx.scene.layout.Background;
      import javafx.scene.layout.BackgroundFill;
      import javafx.scene.layout.CornerRadii;
      import javafx.scene.layout.HBox;
      import javafx.scene.layout.Pane;
      import javafx.scene.layout.StackPane;
      import javafx.scene.layout.VBox;
      import javafx.scene.paint.Color;
      import javafx.scene.shape.Rectangle;
      import javafx.stage.Stage;

      public class DraggableNode01 extends Application {
        private static final CityObject RESIDENCE = new CityObject("RZ", Type.RESIDENCE, Color.GREEN, 2, 2, 0, 0, 0);
        private static final Map<Type, CityObject> CITY_OBJECTS = new LinkedHashMap<>();

        static {
          CITY_OBJECTS.put(Type.ROAD, new CityObject("Road", Type.ROAD, Color.LIGHTGRAY, 1, 1, 0, 0, 0));
          CITY_OBJECTS.put(Type.RESIDENCE, RESIDENCE);
          CITY_OBJECTS.put(Type.RECREATION, new CityObject("P", Type.RECREATION, Color.LIGHTGREEN, 1, 2, 6, 10, 0.25));
          CITY_OBJECTS.put(Type.EDUCATION, new CityObject("Uni", Type.EDUCATION, Color.WHITE, 4, 4, 24, 24, 0.6));
        }

        private static final int GRID_UNIT = 16;
        private static final int GRID_WIDTH = 16 * 50 + 1;

        private final StackPane root = new StackPane();
        private final Pane dragArea = new Pane();
        private final StackPane mapArea = new StackPane();
        private final Pane layoutArea = new Pane();

        @Override
        public void start(Stage primaryStage) {
          HBox mainArea = new HBox();
          VBox palette = new VBox();

          Canvas grid = new Canvas(GRID_WIDTH, GRID_WIDTH);

          GraphicsContext g2d = grid.getGraphicsContext2D();

          g2d.setStroke(Color.LIGHTGRAY);

          for(int y = 0; y < GRID_WIDTH; y += GRID_UNIT) {
            for(int x = 0; x < GRID_WIDTH; x += GRID_UNIT) {
              g2d.strokeLine(x, 0, x, GRID_WIDTH);
            }

            g2d.strokeLine(0, y, GRID_WIDTH, y);
          }

          mapArea.getChildren().addAll(grid, layoutArea);
          mainArea.getChildren().addAll(palette, mapArea);

          palette.getChildren().add(new Label("Palette"));
          palette.getChildren().add(new Button("Palette"));

          ToggleGroup toggleGroup = new ToggleGroup();

          for(CityObject cityObject : CITY_OBJECTS.values()) {
            ToggleButton toggleButton = new ToggleButton(cityObject.shortName);

            toggleButton.setToggleGroup(toggleGroup);
            toggleButton.setUserData(cityObject);
            toggleButton.setMinWidth(50);
            palette.getChildren().add(toggleButton);
          }

          toggleGroup.getToggles().get(0).setSelected(true);

          layoutArea.setMinSize(GRID_WIDTH, GRID_WIDTH);
          layoutArea.setMaxSize(GRID_WIDTH, GRID_WIDTH);

          EventHandler<? super MouseEvent> addRemoveObjectHandler = event -> {
            System.out.println("Dragging..");
            Point2D local = layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY());
      // event.setDragDetect(true);

            if(event.isPrimaryButtonDown()) {
              CityObject cityObject = (CityObject)toggleGroup.getSelectedToggle().getUserData();

              if(!validSpot(local, cityObject, null)) {
                return;
              }

              DraggableNode draggableNode = createDraggableNode(cityObject);

              layoutArea.getChildren().add(draggableNode);

              draggableNode.setLayoutX(snapToGrid(local.getX()));
              draggableNode.setLayoutY(snapToGrid(local.getY()));

              layoutArea.layout();
              updatePopulation();
              save();
            }
            else if(event.isSecondaryButtonDown()) {
              Node node = getNodeAt(local);

              if(node != null) {
                layoutArea.getChildren().remove(node);
                layoutArea.layout();
                updatePopulation();
                save();
              }
            }
          };

          layoutArea.setOnMousePressed(addRemoveObjectHandler);
          layoutArea.setOnMouseDragged(addRemoveObjectHandler);

          root.getChildren().addAll(mainArea, dragArea);
          dragArea.setMouseTransparent(true);

          Scene scene = new Scene(root, 1024, 900, Color.rgb(160, 160, 160));

          // finally, show the stage
          primaryStage.setTitle("Draggable Node 01");
          primaryStage.setScene(scene);
          load();
          primaryStage.show();
        }

        private boolean validSpot(Point2D local, CityObject cityObject, Node draggedNode) {
          for(Node node : layoutArea.getChildren()) {
            if(!node.equals(draggedNode)) {
              Point2D childLocal = node.parentToLocal(snapToGrid(local.getX()), snapToGrid(local.getY()));

              if(node.intersects(childLocal.getX() + 2, childLocal.getY() + 2, GRID_UNIT * cityObject.width - 3, GRID_UNIT * cityObject.height - 3)) {
                return false;
              }
            }
          }

          return true;
        }

        private Node getNodeAt(Point2D local) {
          for(Node node : layoutArea.getChildren()) {
            Point2D childLocal = node.parentToLocal(local);

            if(node.contains(childLocal)) {
              return node;
            }
          }

          return null;
        }

        private void load() {
          try {
            try(BufferedReader reader = Files.newBufferedReader(Paths.get("layout.txt"))) {
              for(;;) {
                String line = reader.readLine();

                if(line == null) {
                  break;
                }

                String[] split = line.split(",");

                int x = Integer.parseInt(split[0]);
                int y = Integer.parseInt(split[1]);
                Type type = Type.valueOf(split[2]);

                DraggableNode draggableNode = createDraggableNode(CITY_OBJECTS.get(type));

                layoutArea.getChildren().add(draggableNode);

                draggableNode.setLayoutX(x * GRID_UNIT);
                draggableNode.setLayoutY(y * GRID_UNIT);
              }
            }

            layoutArea.layout();
            updatePopulation();
          }
          catch(IOException e) {
            System.out.println("Error loading: " + e.getMessage());
          }
          catch(Exception e) {
            System.out.println("Error decoding: " + e.getMessage());
          }
        }

        private void save() {
          try {
            Path tempPath = Files.createTempFile(Paths.get(""), "", "tmp");

            try {
              try(BufferedWriter writer = Files.newBufferedWriter(tempPath)) {
                for(Node node : layoutArea.getChildren()) {
                  DraggableNode draggableNode = (DraggableNode)node;
                  CityObject cityObject = (CityObject)draggableNode.getUserData();

                  writer.write(String.format("%d,%d,%s",
                    (int)(draggableNode.getLayoutX() / GRID_UNIT),
                    (int)(draggableNode.getLayoutY() / GRID_UNIT),
                    cityObject.type.toString()
                  ));
                  writer.newLine();
                }
              }

              Files.move(tempPath, Paths.get("layout.txt"), StandardCopyOption.REPLACE_EXISTING);
            }
            catch(IOException e) {
              Files.delete(tempPath);

              System.out.println("Error saving: " + e.getMessage());
            }
          }
          catch(IOException e) {
            System.out.println("Error creating temp file: " + e.getMessage());
          }
        }

        private void updatePopulation() {
          Type[] types = new Type[] {Type.RECREATION, Type.EDUCATION, Type.TRANSPORTATION, Type.LUXURY};
          Color[] colors = new Color[] {Color.LIGHTGREEN, Color.WHITE, Color.YELLOW, Color.PURPLE};
          Map<Type, Double> populationIncreases = new HashMap<>();

          for(Node residentialNode : layoutArea.getChildren()) {
            if(residentialNode.getUserData().equals(RESIDENCE)) {
              populationIncreases.clear();

              for(Node node : layoutArea.getChildren()) {
                CityObject cityObject = (CityObject)node.getUserData();

                if(cityObject.populationIncrease > 0) {
                  if(residentialNode.getBoundsInParent().intersects(
                    -GRID_UNIT * (cityObject.effectWidth - cityObject.width) / 2 + node.getLayoutX() + 2,
                    -GRID_UNIT * (cityObject.effectHeight - cityObject.height) / 2 + node.getLayoutY() + 2,
                    GRID_UNIT * cityObject.effectWidth - 4,
                    GRID_UNIT * cityObject.effectHeight - 4
                  )) {
                    populationIncreases.merge(cityObject.type, cityObject.populationIncrease, Double::sum);
                  }
                }
              }

              DraggableNode draggableNode = (DraggableNode)residentialNode;

              draggableNode.getChildren().clear();

              for(int i = 0; i < types.length; i++) {
                Type type = types[i];
                Double populationIncrease = populationIncreases.get(type);

                if(populationIncrease != null) {
                  populationIncrease = Math.min(1.0, populationIncrease);
                  double height = (GRID_UNIT * 2 - 1) * populationIncrease;
                  Rectangle rectangle = new Rectangle(4 + 6 * i, GRID_UNIT * 2 - height, 4, height);

                  rectangle.setFill(colors[i]);

                  draggableNode.getChildren().add(rectangle);
                }
              }
            }
          }
        }

        private DraggableNode createDraggableNode(CityObject cityObject) {
          DraggableNode node = new DraggableNode();
          node.setPrefSize(cityObject.width * GRID_UNIT + 1, cityObject.height * GRID_UNIT + 1);
          // define the style via css

          node.setUserData(cityObject);
          node.setBackground(new Background(new BackgroundFill(cityObject.color, CornerRadii.EMPTY, Insets.EMPTY)));
          node.setStyle(
              "-fx-text-fill: black; "
              + "-fx-border-color: black;");
          return node;
        }

        public static void main(String[] args) {
            launch(args);
        }

        /**
         * Simple draggable node.
         *
         * Dragging code based on {@link http://blog.ngopal.com.np/2011/06/09/draggable-node-in-javafx-2-0/}
         *
         * @author Michael Hoffer <info@michaelhoffer.de>
         */
        class DraggableNode extends Pane {

          // mouse position
          private Node view;
          private double originalX;
          private double originalY;
          private boolean dragging = false;
          private boolean moveToFront = true;

          private Point2D dragHandlePosition;

          public DraggableNode() {
              init();
          }

          public DraggableNode(Node view) {
              this.view = view;

              getChildren().add(view);
              init();
          }

            private void init() {

                onMousePressedProperty().set(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent event) {
                      if(!event.isPrimaryButtonDown()) {
                        return;
                      }

                      dragHandlePosition = sceneToLocal(event.getSceneX(), event.getSceneY());

                      System.out.println("Yes");

                      // record the drag handle position

                      Point2D layoutAreaPosition = layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY());
                      setLayoutX(snapToGrid(layoutAreaPosition.getX() - dragHandlePosition.getX()));
                      setLayoutY(snapToGrid(layoutAreaPosition.getY() - dragHandlePosition.getY()));

                      setOpacity(0.7);

                      originalX = getLayoutX();
                      originalY = getLayoutY();

                      CityObject cityObject = (CityObject)getUserData();

                      if(cityObject.effectWidth != 0) {
                        Rectangle rectangle = new Rectangle(-GRID_UNIT * (cityObject.effectWidth - cityObject.width) / 2, -GRID_UNIT * (cityObject.effectHeight - cityObject.height) / 2, GRID_UNIT * cityObject.effectWidth, GRID_UNIT * cityObject.effectHeight);

                        rectangle.setOpacity(0.3);

                        getChildren().add(rectangle);
                      }

                      if (isMoveToFront()) {
                          toFront();
                      }

                      updatePopulation();
                    }
                });

                //Event Listener for MouseDragged
                onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
                  @Override
                  public void handle(MouseEvent event) {
                    if(!event.isPrimaryButtonDown()) {
                      return;
                    }

                    Point2D layoutAreaPosition = layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY());
                    double x = snapToGrid(layoutAreaPosition.getX() - dragHandlePosition.getX());
                    double y = snapToGrid(layoutAreaPosition.getY() - dragHandlePosition.getY());
                    boolean validSpot = validSpot(new Point2D(x, y), (CityObject)getUserData(), DraggableNode.this);

                    setLayoutX(x);
                    setLayoutY(y);

                    dragging = true;

                    setVisible(layoutArea.contains(layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY())));
                    setEffect(validSpot ? null : new DropShadow(5, Color.RED));

                    updatePopulation();
                    event.consume();
                  }
                });

                onMouseReleasedProperty().set(new EventHandler<MouseEvent>() {
                  @Override
                  public void handle(MouseEvent event) {
                    if(!event.isPrimaryButtonDown()) {
                      return;
                    }

                    System.out.println("Drag done");
                    Point2D layoutAreaPosition = layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY());
                    double x = snapToGrid(layoutAreaPosition.getX() - dragHandlePosition.getX());
                    double y = snapToGrid(layoutAreaPosition.getY() - dragHandlePosition.getY());
                    boolean validSpot = validSpot(new Point2D(x, y), (CityObject)getUserData(), DraggableNode.this);

                    if(layoutArea.contains(layoutArea.sceneToLocal(event.getSceneX(), event.getSceneY())) && validSpot) {

                      System.out.println("Positioning X as: " + snapToGrid(event.getSceneX() - dragHandlePosition.getX()));
                      setLayoutX(x);
                      setLayoutY(y);
                    }
                    else {
                      setLayoutX(originalX);
                      setLayoutY(originalY);
                    }

                    setEffect(null);
                    setOpacity(1.0);
                    getChildren().clear();

                    dragging = false;
                    updatePopulation();
                    save();
                  }
                });

            }

            /**
             * @return the dragging
             */
            protected boolean isDragging() {
                return dragging;
            }


            /**
             * @return the view
             */
            public Node getView() {
                return view;
            }

            /**
             * @param moveToFront the moveToFront to set
             */
            public void setMoveToFront(boolean moveToFront) {
                this.moveToFront = moveToFront;
            }

            /**
             * @return the moveToFront
             */
            public boolean isMoveToFront() {
                return moveToFront;
            }

            public void removeNode(Node n) {
                getChildren().remove(n);
            }
        }

        private static double snapToGrid(double x) {
          if(x < 0) {
            return 0;
          }
          return Math.floor(x / 16.0) * 16.0;
        }

        public static class CityObject {
          public enum Type {ROAD, RESIDENCE, RECREATION, EDUCATION, TRANSPORTATION, LUXURY}
          public final String shortName;
          public final Type type;
          public final Color color;
          public final int width;
          public final int height;
          public final int effectWidth;
          public final int effectHeight;
          public final double populationIncrease;

          public CityObject(String shortName, Type type, Color color, int width, int height, int effectWidth, int effectHeight, double populationIncrease) {
            this.shortName = shortName;
            this.type = type;
            this.color = color;
            this.width = width;
            this.height = height;
            this.effectWidth = effectWidth;
            this.effectHeight = effectHeight;
            this.populationIncrease = populationIncrease;
          }
        }
      }

        Attachments

          Activity

            People

            • Assignee:
              ekleyman Elina Kleyman (Inactive)
              Reporter:
              jhendrikx John Hendrikx
            • Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved:
                Imported: