My idea for the tic-tac-toe grid is to have it work as though there were a couple of bins of Xs and Os on the right and the player will drag the token he wants on to the square he wants.
I’ve assigned the controller for the game to TttController and set up IDs for the elements.
@FXML
public GridPane xobin;
@FXML
public ImageView X;
@FXML
public ImageView O;
@FXML
public GridPane board;
The xobin is where the Xs and Os will live. I’ve added one ImageView to each cell in the xobin, with a picture of an X and O, naturally.
Drags in JavaFX work through this thing called the Dragboard, which is like the clipboard but…for dragging. You even actually create Clipboard objects to go into the dragboard. So when we detect a drag:
public void handleOnDragDetected(MouseEvent event) {
System.out.println("Drag. ");
Dragboard db = X.startDragAndDrop(TransferMode.ANY);
ClipboardContent content = new ClipboardContent();
content.putString("X");
db.setContent(content);
}
We create a clipboard object and put an “X” in there (or an “O” presumably, eventually), and we’ll be able to read that content on the drop side. (Don’t forget to set the OnDragDetected event of your X imageview.)
But before we get to dropping anything, we have to know where we are. And that’s where things start to get weird. It’s easy enough to get the coordinate within the grid pane:
public void handleOnDragOver(DragEvent event) {
System.out.println("EVENT: " + event.getX() + "," + event.getY());
But this only gives us the X,Y location (0,0 is upper left) within the GridPane. There are handy little calls to get the row and column of a particular object in a GridPane:
Node node = (Node) event.getTarget();
int row = GridPane.getRowIndex(node);
int column = GridPane.getColumnIndex(node);
System.out.println("X: " + row + ", Y:" + column);
But you will see a printout that’s basically gibberish around an exception message, something like:
J a v a M e s s a g e : j a v a . l a n g . r e f l e c t . I n v o c a t i o n T a r g e t E x c e p t i o n
Why? Because there are no nodes in our GridPane. My assumption was that the target (returned by event.getTarget) would be the cell we were hovering over when in fact it is the entire GridPane.
Well, okay, we have the X and Y, there’s probably a GridPane call to return the column and row for a given coordinate, right?
Wrong.
You can get the grid column and row from a given node that already exists inside the grid. That is, if we put down an ImageView, say, into each grid we could determine which ImageView the mouse was under using getRowIndex and getColumnIndex.
At least, theoretically. I haven’t been able to make it work in my experiments yet. I also don’t want to put down nine empty ImageViews, and map all their handle events separately to the same (or worse, different) events. This is one of those frustrating things because what we want to do, basically, is just exactly what SceneBuilder does.
Like, if we drag a button over a cell grid, what happens?
The particular cell highlights, showing us where we’re going to drop the button.
Now, when I hit a wall like this, I find there’s always the smart thing to learn, and the dumb thing to learn. The smart thing is this: Cell Grid is meant to be a layout, not a control per se. You can carry this through to all the layouts, presumably: The user isn’t supposed to interact with the layout directly, and trying to shoehorn the existing GridPane into an end-user object will probably end in tears.
I haven’t done a deep dive into SceneBuilder, but I think it may actually create nodes for the GridPane as it goes, and while these nodes don’t end up in the FXML (which is, after all, just a text file), they serve as the basis for the user’s interaction with the layout in SceneBuilder.
But sometimes, you just gotta say, “what the heck”.