3

Note: 25 years with Java, 2.5 hours (almost) with JavaFX.

I want to be able to highlight all cells of a GridPane that the mouse is dragged across - i.e. everything that intersects with the rectangle cornered by the click point and the current drag point. I can do that if all children are 1x1, but with mixed sizes I'm having no joy.

For example, if the top row has 1 1 column cell (A) and 1 2 column cell (B) and the bottom has 1 2 column cell (C) and 1 1 column cell (D), if I click in A and drag down into C I can highlight both. However, I cannot figure out when I drag into the right half of C so that B should be highlighted.

Sample Board:

sample board

Apologies for the HSCE - it's a bit long but I felt stripping it down would reduce readability.

import java.util.*;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent; 
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.Node;
import javafx.stage.Stage;
import javafx.geometry.*;

public class AddTestSSCE extends Application
{
    ArrayList<StackPane> sPanes = new ArrayList<StackPane>(); // selected panes
    GridPane theGrid;
    Node startNode = null;
    int col0, col1;
    int row0, row1;

    @Override
    public void start(Stage stage) {
        theGrid = new GridPane();
        ColumnConstraints col = new ColumnConstraints(300);
        theGrid.getColumnConstraints().addAll(col, col, col);
        RowConstraints row = new RowConstraints(200);
        theGrid.getRowConstraints().addAll(row, row, row);
        addGridPane();
        theGrid.getStyleClass().add("bg-grid");
        Scene scene = new Scene(theGrid, 1024, 768);
        scene.getStylesheets().add("addtestssce.css");
        stage.setScene(scene);
        stage.show();
    }

    public void addGridPane() {
        theGrid.setHgap(10);
        theGrid.setVgap(10);
        theGrid.setPadding(new Insets(0, 10, 0, 10));

        StackPane theSP = sPAdd(new Label("A"));
        theGrid.add(theSP, 0, 0, 1, 1); 

        theSP = sPAdd(new Label("B"));
        theGrid.add(theSP, 1, 0, 2, 1);

        theSP = sPAdd(new Label("C"));
        theGrid.add(theSP, 0, 1, 2, 1);

        theSP = sPAdd(new Label("D"));
        theGrid.add(theSP, 2, 1, 1, 1);

        theGrid.addEventFilter(MouseEvent.MOUSE_PRESSED,     //Creating the mouse event handler 
        new EventHandler<MouseEvent>() { 
            @Override 
            public void handle(MouseEvent e) { 
                System.out.println("We're Moving!!");

                startNode = (Node)e.getTarget();
                sPanes.add((StackPane)startNode);
                col0 = GridPane.getColumnIndex(startNode).intValue();
                row0 = GridPane.getRowIndex(startNode).intValue();
                System.out.printf("Starting at %d %d\n", col0, row0);
            }
        });

        theGrid.addEventFilter(MouseEvent.MOUSE_DRAGGED,     //Creating the mouse event handler 
        new EventHandler<MouseEvent>() { 
            Node lastNode = null;

            @Override 
            public void handle(MouseEvent e) { 
                Node target = (Node)e.getTarget();
                double xLoc = e.getX();
                double yLoc = e.getY();
                Bounds bs = target.localToScene(target.getBoundsInLocal());
                Node moveTarget;
                if( bs.contains(xLoc, yLoc) )
                {
                    moveTarget = target;
                }
                else
                {
                    moveTarget = getContainingNode((int)xLoc, (int)yLoc);
                }
                if( moveTarget != null && lastNode != moveTarget )
                {
                    col1 = GridPane.getColumnIndex(moveTarget).intValue();
                    row1 = GridPane.getRowIndex(moveTarget).intValue();
                    doHighlighting();
                    lastNode = moveTarget;
                }
            }
        });
    }

    void doHighlighting()
    {
        int c0, c1, r0, r1;

        c0 = col0 > col1 ? col1 : col0;
        c1 = !(col0 > col1) ? col1 : col0;
        r0 = row0 > row1 ? row1 : row0;
        r1 = !(row0 > row1) ? row1 : row0;

        Rectangle2D rec1 = new Rectangle2D(c0, r0, c1-c0+1, r1-r0+1);

        System.out.printf("Box: %d %d %d %d\n", c0, c1, r0, r1);
        List<Node> nodes = theGrid.getChildren();
        for( Node node : nodes )
        {
            StackPane sp = (StackPane)node;
            unhighlight(sp);
            int col = GridPane.getColumnIndex(sp).intValue();
            int row = GridPane.getRowIndex(sp).intValue(); 

            if( occupiesCell(sp, rec1) )
            {
                highlight(sp);
            }
        }
    }

    boolean occupiesCell(Node node, Rectangle2D r1)
    {
        boolean result = false;
        int col = GridPane.getColumnIndex(node).intValue();
        int row = GridPane.getRowIndex(node).intValue(); 
        int wid = GridPane.getColumnSpan(node).intValue();
        int hei = GridPane.getRowSpan(node).intValue(); 

        Rectangle2D r2 = new Rectangle2D( col, row, wid, hei);

        return r2.intersects(r1);
    }

    void unhighlight(Node node)
    {
        if( !(node instanceof StackPane) )
        {
            return;
        }
        StackPane label = (StackPane)node;
        List<String> cList = label.getStyleClass();
        cList.remove("b2");
        cList.add("b1");
    }

    void highlight(Node node)
    {
        if( !(node instanceof StackPane) )
        {
            return;
        }
        StackPane label = (StackPane)node;
        List<String> cList = label.getStyleClass();
        cList.remove("b1");
        cList.add("b2");
    }

    private Node getContainingNode(int xLoc, int yLoc)
    {
        Node tgt = null;

        for( Node node : theGrid.getChildren() )
        {
            Bounds boundsInScene = node.localToScene(node.getBoundsInLocal());
            if( boundsInScene.contains(xLoc, yLoc) )
            {
                return node;
            }
        }

        return tgt;
    }

    private StackPane sPAdd(Label label)
    {
        StackPane gPPane = new StackPane();
        gPPane.getChildren().add(label);
        gPPane.getStyleClass().addAll("b1", "grid-element");
        GridPane.setFillHeight(gPPane, true);
        GridPane.setFillWidth(gPPane, true);

        return gPPane;
    }

    public static void main(String[] args)
    {
        launch();
    }
}
.bg-grid {
    -fx-background-color: slategrey;
}
.grid-element {
    -fx-border-width: 10; 
    -fx-border-color: rgb(225, 128, 217);
    -fx-background-color: rgb(247, 146, 146);
    -fx-font: 36 arial;
}

.b1 {
    -fx-text-base-color: white;
    -fx-border-color: rgb(225, 128, 217);
}
.b2 {
    -fx-text-base-color: lightgray;
    -fx-border-color: rgb(233, 228, 86);
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Isn’t an easier strategy to register mouse handlers with each of the cells (`StackPane`s), instead of registering a handler with the grid? And even with your strategy, do you actually need to get your hands dirty with coordinates at all? Isn’t the target you get a reference to simply the node you want to highlight/unhighlight anyway? – James_D Sep 23 '21 at 18:46
  • I tried multiple handlers but would not register when dragging from one cell to the other. The problem with determining column location within multi column cells remains though. – didntgoboom Sep 23 '21 at 18:49
  • 1
    For multiple handlers, I think you have to call `startFullDrag()` at some point. (I can’t access the Java docs easily at the moment.) And I understand the problem now. So perhaps this is basically the correct approach, but just compute the coordinates of the rectangle drawn by the mouse relative to the grid (as you already do), create a `BoundingBox` representing that rectangle, then iterate over all children of the grid and check if their `boundsInParent()` intersects that bounding box. – James_D Sep 23 '21 at 18:55
  • So I basically have to do a pixel based rectangular search rather than a row/column like I'm doing? – didntgoboom Sep 23 '21 at 18:59
  • 1
    You only need the pixel location to create the `BoundingBox`. It has a `boolean intersects(Bounds)` method you can call, passing each child node’s `boundInParent()` – James_D Sep 23 '21 at 19:05
  • Are you looking for something like this? https://stackoverflow.com/questions/60012383/mousedragged-detection-for-multiple-nodes-while-holding-the-button-javafx – Slaw Sep 23 '21 at 19:09
  • Same kind of thing, but need to be able to tell where the mouse is in multi column/row drags. – didntgoboom Sep 23 '21 at 19:19
  • I guess I'm still not sure where the problem is. If you add an `onMouseDragEntered` handler to each `StackPane` (i.e. cell), then you shouldn't need to mess around with coordinates. – Slaw Sep 23 '21 at 19:43
  • I don't know if you can see the image I posted, but the simpler case is 2 cells one column wide over a one cell two columns wide. If I start dragging in the cell at (0,0), then drag straight down in to the cell at (0,1), I need to be able to tell if I drag to the right so that the cell at (1,0) is selected as well. Can't do that with mouseenter etc events. – didntgoboom Sep 23 '21 at 20:09
  • Ah, I understand now. Then I believe James's suggestion of bounds and testing intersection should work for you. – Slaw Sep 23 '21 at 20:30
  • @James_D that worked great! Thanks - even managed to cut a bit of code out. 191 to 174 lines to be precise. (Nondestructive) code reduction is always a good thing 8-) – didntgoboom Sep 23 '21 at 20:48
  • @didntgoboom You can probably significantly reduce that further, eg by using a CSS pseudoclass instead of adding and removing classes, etc. It becomes something like `private static final PseudoClass highlight = PseudoClass.getPseudoClass(“highlight”);` and then `BoundingBox bb = new BoundingBox(…); for (Node child : grid.getChildren()) child.pseudoclassStateChanged(highlight, bb.intersects(child.getBoundsInParent());` Then change your CSS to define rules for `.grid-element` and `.grid-element:highlight` – James_D Sep 23 '21 at 20:54
  • Point of order - this is my first post on SE. Is there an appropriate place to post the final working code? – didntgoboom Sep 23 '21 at 20:59
  • 1
    @didntgoboom Yes, you can answer your own question (and accept it as the working answer after some time). – James_D Sep 23 '21 at 21:01
  • @James_D - super cool!! Maybe this JavaFX thing is going to work out OK after all!! – didntgoboom Sep 23 '21 at 21:11

1 Answers1

2

After a bit of back and forth with @james_d and @slaw, finally came up with a working solution.

import java.util.*;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent; 
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.Node;
import javafx.stage.Stage;
import javafx.geometry.*;
import javafx.css.PseudoClass;

public class AddTestSSCE extends Application
{
    private static final PseudoClass highlight = PseudoClass.getPseudoClass("highlight"); 
    ArrayList<StackPane> sPanes = new ArrayList<StackPane>(); // selected panes
    GridPane theGrid;
    Node startNode = null;
    int x0, y0, x1, y1;

    @Override
    public void start(Stage stage) {
        theGrid = new GridPane();
        ColumnConstraints col = new ColumnConstraints(300);
        theGrid.getColumnConstraints().addAll(col, col, col);
        RowConstraints row = new RowConstraints(200);
        theGrid.getRowConstraints().addAll(row, row, row);
        addGridPane();
        theGrid.getStyleClass().add("bg-grid");
        Scene scene = new Scene(theGrid, 1024, 768);
        scene.getStylesheets().add("addtestssce.css");
        stage.setScene(scene);
        stage.show();
    }

    public void addGridPane() {
        theGrid.setHgap(10);
        theGrid.setVgap(10);
        theGrid.setPadding(new Insets(0, 10, 0, 10));

        StackPane theSP = sPAdd(new Label("A"));
        theGrid.add(theSP, 0, 0, 1, 1); 

        theSP = sPAdd(new Label("B"));
        theGrid.add(theSP, 1, 0, 2, 1);

        theSP = sPAdd(new Label("C"));
        theGrid.add(theSP, 0, 1, 2, 1);

        theSP = sPAdd(new Label("D"));
        theGrid.add(theSP, 2, 1, 1, 1);

        theGrid.addEventFilter(MouseEvent.MOUSE_PRESSED,     //Creating the mouse event handler 
        new EventHandler<MouseEvent>() { 
            @Override 
            public void handle(MouseEvent e) { 
                System.out.println("We're Moving!!");

                startNode = (Node)e.getTarget();
                sPanes.add((StackPane)startNode);
                x0 = x1 = (int)e.getX();
                y0 = y1 = (int)e.getY();
                doHighlighting();
                System.out.printf("Starting at %d %d\n", x0, y0);
            }
        });

        theGrid.addEventFilter(MouseEvent.MOUSE_DRAGGED,     //Creating the mouse event handler 
        new EventHandler<MouseEvent>() { 

            @Override 
            public void handle(MouseEvent e) { 
                Node target = (Node)e.getTarget();
                x1 = (int)e.getX();
                y1 = (int)e.getY();
                Bounds bs = target.localToScene(target.getBoundsInLocal());
                Node moveTarget;
                if( bs.contains(x1, y1) )
                {
                    moveTarget = target;
                }
                else
                {
                    moveTarget = getContainingNode( x1, y1);
                }
                if( moveTarget != null )
                {
                    doHighlighting();
                }
            }
        });
    }

    void doHighlighting()
    {
        int c0, c1, r0, r1;

        c0 = x0 > x1 ? x1 : x0;
        c1 = !(x0 > x1) ? x1 : x0;
        r0 = y0 > y1 ? y1 : y0;
        r1 = !(y0 > y1) ? y1 : y0;

        Bounds dragged = new BoundingBox(c0, r0, c1-c0+1, r1-r0+1);

        for (Node child : theGrid.getChildren()) 
        {
            child.pseudoClassStateChanged(highlight, dragged.intersects(child.getBoundsInParent())); 
        }
    }

    private Node getContainingNode(int xLoc, int yLoc)
    {
        Node tgt = null;

        for( Node node : theGrid.getChildren() )
        {
            Bounds boundsInScene = node.localToScene(node.getBoundsInLocal());
            if( boundsInScene.contains(xLoc, yLoc) )
            {
                return node;
            }
        }

        return tgt;
    }

    private StackPane sPAdd(Label label)
    {
        StackPane gPPane = new StackPane();
        gPPane.getChildren().add(label);
        gPPane.getStyleClass().addAll("b1", "grid-element");
        GridPane.setFillHeight(gPPane, true);
        GridPane.setFillWidth(gPPane, true);

        return gPPane;
    }

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

and the CSS:

.bg-grid {
    -fx-background-color: slategrey;
}
.grid-element {
    -fx-border-width: 10; 
    -fx-border-color: rgb(225, 128, 217);
    -fx-background-color: rgb(247, 146, 146);
    -fx-font: 36 arial;
}

.grid-element:highlight {
    -fx-text-base-color: lightgray;
    -fx-border-color: rgb(233, 228, 86);
}
  • Do you still need `startNode`, `target`, `getContainingNode()`, etc? Those just look redundant now. – James_D Sep 24 '21 at 12:03