I tried almost everything, but the mouse drag events are not firing, like explained here:
https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/input/MouseDragEvent.html
Here is a minimal example, so you can try it out (I am using Java 11 with JavaFX 11.0.2):
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class Main extends Application {
private double mouseClickPositionX, mouseClickPositionY, currentRelativePositionX, currentRelativePositionY;
#Override
public void start(Stage primaryStage) throws Exception{
primaryStage.setTitle("Hello World");
BorderPane mainBorderPane = new BorderPane();
BorderPane centerBorderPane = new BorderPane();
FlowPane flowPane = new FlowPane();
GridPane gridPane = new GridPane();
Button button1 = new Button("button1");
gridPane.add(button1, 0, 0);
flowPane.getChildren().add(gridPane);
centerBorderPane.setCenter(flowPane);
HBox hbox = new HBox();
TilePane tilePane = new TilePane();
Button button2 = new Button("button2");
tilePane.getChildren().add(button2);
hbox.getChildren().add(tilePane);
mainBorderPane.setCenter(centerBorderPane);
centerBorderPane.setBottom(hbox);
// button2 event handlers
button2.setOnMousePressed(event -> {
mouseClickPositionX = event.getSceneX();
mouseClickPositionY = event.getSceneY();
currentRelativePositionX = button2.getTranslateX();
currentRelativePositionY = button2.getTranslateY();
button2.setMouseTransparent(true);
});
button2.setOnMouseDragged(event -> {
button2.setTranslateX(currentRelativePositionX + (event.getSceneX() - mouseClickPositionX));
button2.setTranslateY(currentRelativePositionY + (event.getSceneY() - mouseClickPositionY));
});
button2.setOnDragDetected(event -> {
button2.startFullDrag();
});
button2.setOnMouseReleased((event) -> {
button2.setMouseTransparent(false);
});
// button1 event handlers
button1.setOnMouseDragReleased((event) -> {
System.out.println("it works in button1");
});
// gridPane event handlers
gridPane.setOnMouseDragReleased((event) -> {
System.out.println("it works in gridPane");
});
primaryStage.setScene(new Scene(mainBorderPane, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I want to get the reference of button2 either in button1 or in gridPane via setOnMouseDragReleased. There are many nested panes etc. because I wanted to maintain the original project layout structure. I did this because I am not sure if this also can be a reason for the non functioning.
Thanks in advance.
I've ended up manually triggering the events from centerBorderPane to gridPane, using node.fireEvent(event). Also implemented a helper function, which returns the right child node:
private Optional<Node> findNode(Pane pane, double x, double y) {
return pane.getChildren().stream().filter(n -> {
Point2D point = n.sceneToLocal(x, y);
return n.contains(point.getX(), point.getY());
}).findAny();
}
Don't forget to consume the events, so you won't get into an infinite loop.
MOUSE_DRAG_RELEASED fires when a drag ends on this node. For example
centerBorderPane.setOnMouseDragReleased((event) -> {
System.out.println("centerBorderPane drag released");
});
should fire when you drag button2 and the drag ends on centerBorderPane.
To fire an event when the mouse is dragged over button1 use button1.setOnMouseDragged
If you want to propagate a mouse event from parent to its children see this
Related
I have some JavaFX Popup in my application. And when any of these popups is foucsed, I need it bring on top of every other popups regardless of it's index in Window.getWindows().
I've tried to call method like toFront but it's not in Popup class. I've also tried to change index of focused Popup in Window.getWindows() but that also didn't worked because I don't know how to interchange index of two elements in a ObservableList.
e.g.
Let's say I have two Popup called p1 and p2 and in each I have nodes n1 and n2 respectively which are used to move these popup, So whenever n1 is dragged p1 should come on top and when n2 is dragged p2 should come on top.
Here is my minimal example:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Popup;
import javafx.stage.Stage;
public class Example extends Application{
public static void main(String... arguments){
launch(arguments);
}
public void applyTo(Pane node, Popup parent){
final double[] dragDelta = new double[2];
node.setOnMousePressed(e -> {
dragDelta[0] = parent.getX() - e.getScreenX();
dragDelta[1] = parent.getY() - e.getScreenY();
//code to bring parent Popup to front
});
node.setOnMouseDragged(e -> {
parent.setX(e.getScreenX() + dragDelta[0]);
parent.setY(e.getScreenY() + dragDelta[1]);
});
}
#Override
public void start(Stage primaryStage) throws Exception{
Button b1 = new Button("Open p1");
Button b2 = new Button("Open p2");
HBox n1 = new HBox(new Label("This is p1"));
HBox n2 = new HBox(new Label("This is p2"));
n1.setMinSize(200, 120);
n2.setMinSize(200, 120);
n1.setStyle("-fx-background-color: blue; -fx-background-radius: 4px;");
n2.setStyle("-fx-background-color: red; -fx-background-radius: 4px;");
n1.setAlignment(Pos.CENTER);
n2.setAlignment(Pos.CENTER);
Popup p1 = new Popup();
Popup p2 = new Popup();
p1.getContent().add(n1);
p2.getContent().add(n2);
applyTo(n1, p1);
applyTo(n2, p2);
b1.setOnAction(event -> {
if(!p1.isShowing()) p1.show(primaryStage);
else p1.hide();
});
b2.setOnAction(event -> {
if(!p2.isShowing()) p2.show(primaryStage);
else p2.hide();
});
HBox root = new HBox(10, b1, b2);
root.setAlignment(Pos.CENTER);
primaryStage.setScene(new Scene(root, 500, 200));
primaryStage.show();
}
}
So what is the solution for this problem?
For some reason I don't understand, toFront/back is only implemented on Stage, not on its parent classes even though the actual collaborator that manages the stacking is already available in Window:
The implementation in Stage:
/**
* Bring the {#code Window} to the foreground. If the {#code Window} is
* already in the foreground there is no visible difference.
*/
public void toFront() {
if (getPeer() != null) {
getPeer().toFront();
}
}
getPeer() is a package-private method in Window that returns the internal class TKStage. So if you are allowed to go dirty (because accessing an internal class and having to access via reflection - all with the usual loud "Beware"!) would be:
protected void toFront(Popup popup) {
// use your favorite utility method to invoke a method
TKStage peer = (TKStage) FXUtils.invokeGetMethodValue(Window.class, popup, "getPeer");
if (peer != null) {
peer.toFront();
}
}
Requires to export/open not-exported packages in javafx.graphics - compiler and runtime errors will guide you (my context is heavily tweaked anyway, so don't know exactly which are added by this)
Here is the solution with stages it is the only work around I have found at all even though you hate the idea of having multiple stages if you want the functionality this is it. If you decide to stick with leaving them in the background thats cool too. An idea to solve your too may stages dilemma is to use a queue of stages remove when in use and if all are shown add a new one when a stage is hidden send to the end of the queue
public class Example extends Application {
public void applyTo(Pane node, Stage parent, Stage primaryStage){
final double[] dragDelta = new double[2];
node.setOnMousePressed(e -> {
dragDelta[0] = parent.getX() - e.getScreenX();
dragDelta[1] = parent.getY() - e.getScreenY();
//code to bring parent Popup to front
});
node.setOnMouseDragged(e -> {
parent.setX(e.getScreenX() + dragDelta[0]);
parent.setY(e.getScreenY() + dragDelta[1]);
primaryStage.requestFocus();
});
node.setOnMouseReleased(event -> {
primaryStage.requestFocus();
});
}
#Override
public void start(Stage primaryStage) throws Exception{
Button b1 = new Button("Open p1");
Button b2 = new Button("Open p2");
HBox n1 = new HBox(new Label("This is p1"));
HBox n2 = new HBox(new Label("This is p2"));
n1.setMinSize(200, 120);
n2.setMinSize(200, 120);
n1.setStyle("-fx-background-color: blue; -fx-background-radius: 4px;");
n2.setStyle("-fx-background-color: red; -fx-background-radius: 4px;");
n1.setAlignment(Pos.CENTER);
n2.setAlignment(Pos.CENTER);
Stage p1 = new Stage(StageStyle.UNDECORATED);
Stage p2 = new Stage(StageStyle.UNDECORATED);
p1.setAlwaysOnTop(true);
p2.setAlwaysOnTop(true);
p1.setScene(new Scene(n1));
p2.setScene(new Scene(n2));
applyTo(n1, p1, primaryStage);
applyTo(n2, p2, primaryStage);
b1.setOnAction(event -> {
if(!p1.isShowing()) {
p1.show();
primaryStage.requestFocus();
}
else
p1.hide();
});
b2.setOnAction(event -> {
if(!p2.isShowing()) {
p2.show();
primaryStage.requestFocus();
}
else
p2.hide();
});
HBox root = new HBox(10, b1, b2);
root.setAlignment(Pos.CENTER);
primaryStage.setScene(new Scene(root, 500, 200));
primaryStage.show();
}
public static void main(String[] args) { launch(args); }
}
I am currently making a paint application and have created several tools which are working, but I encountered a problem when trying to create a
"Draw straight line" tool
So I basically draw a line from point A to B and it works, the line is there, however, when I toggle my other tools (Draw circle, rectangle etc) the shapes are being drawn at the same time as the straight-line despite the "Draw Line" button being toggled off.
The code below will allow you to draw straight-lines and you can try toggling on and off the different buttons, the straight line will still be drawn when you drag the cursor across the pane.
Anyone know what kind of mistake I did, and any possible fixes and/or alternate solutions?
(The event handler is there so that I can select the drawn shapes change them later if needed, this code is a stripped-down version of my paint application)
public class DrawLine extends Application {
#Override
public void start(Stage primaryStage) {
ToggleButton lineButton = new ToggleButton ("Draw Line");
ToggleButton Button = new ToggleButton ("Button with no function");
BorderPane pane = new BorderPane();
ToolBar toolbar = new ToolBar();
Scene scene = new Scene(pane, 1200, 800);
pane.setLeft(toolbar);
toolbar.getItems().addAll(lineButton, Button);
// Draw Line
scene.addEventHandler(MouseEvent.MOUSE_CLICKED, me -> {
if(lineButton.isSelected() & me.getButton().equals(MouseButton.PRIMARY) ) {
scene.setOnMousePressed(event -> {
Line line = new Line();
line.setStartX(event.getX());
line.setStartY(event.getY());
scene.setOnMouseDragged(e->{
line.setEndX(e.getX());
line.setEndY(e.getY());
});
pane.getChildren().add(line);
});
}
});
primaryStage.setTitle("Paint App");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
You only check if the lineButton is selected inside the MOUSE_CLICKED (which is a press-then-release gesture, by the way) handler. Inside this handler you add a MOUSE_PRESSED handler and inside that handler you add a MOUSE_DRAGGED handler. You don't check if the lineButton is selected inside the MOUSE_PRESSED or MOUSE_DRAGGED handlers.
What this all means is that, after the if condition inside the MOUSE_CLICKED handler evaluates to true, you'll have a MOUSE_PRESSED and MOUSE_DRAGGED handler that operate independently of your MOUSE_CLICKED handler. Now, whenever you press any mouse button it will create a Line and add it to the parent. Then the newly added MOUSE_DRAGGED handler will alter the Line.
You're fortunate, in a way, that you're using the onXXX properties instead of using addEventHandler. The properties replace the old EventHandler when set. If that didn't happen (such as with addEventHandler) you'd have many (one more each time) EventHandlers drawing Lines.
You just need to register all the appropriate EventHandlers once and do the logic inside of them.
Here's a small example:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
private ToggleGroup toggleGroup;
private ToggleButton lineBtn;
private Group group;
private Line currentLine;
#Override
public void start(Stage primaryStage) {
toggleGroup = new ToggleGroup();
lineBtn = new ToggleButton("Line");
ToggleButton noneBtn = new ToggleButton("None");
toggleGroup.getToggles().addAll(lineBtn, noneBtn);
toggleGroup.selectToggle(noneBtn);
group = new Group();
BorderPane root = new BorderPane(new Pane(group), new ToolBar(lineBtn, noneBtn), null, null, null);
root.getCenter().setOnMousePressed(this::handleMousePressed);
root.getCenter().setOnMouseDragged(this::handleMouseDragged);
root.getCenter().setOnMouseReleased(this::handleMouseReleased);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.setTitle("Draw Shape Example");
primaryStage.show();
}
private void handleMousePressed(MouseEvent event) {
if (lineBtn.equals(toggleGroup.getSelectedToggle())
&& event.getButton() == MouseButton.PRIMARY) {
currentLine = new Line(event.getX(), event.getY(), event.getX(), event.getY());
group.getChildren().add(currentLine);
}
}
private void handleMouseDragged(MouseEvent event) {
if (currentLine != null) {
currentLine.setEndX(event.getX());
currentLine.setEndY(event.getY());
}
}
private void handleMouseReleased(MouseEvent event) {
if (currentLine != null
&& currentLine.getStartX() == currentLine.getEndX()
&& currentLine.getStartY() == currentLine.getEndY()) {
// The line has no length, remove it
group.getChildren().remove(currentLine);
}
currentLine = null;
}
}
I have a SplitMenuButton, and I can't seem to find a way to trigger an event when the user clicks the arrow next to the button.
I would like the dropdown to fill with items from a database when the dropdown arrow is clicked.
I am not sure which event can do that, and I can not find any info on this either.
Short answer: register a listener with the showing property.
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class SplitMenuButtonTest extends Application {
#Override
public void start(Stage primaryStage) {
IntegerProperty count = new SimpleIntegerProperty();
SplitMenuButton splitMenuButton = new SplitMenuButton();
splitMenuButton.setText("Action");
splitMenuButton.showingProperty().addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
int c = count.get() + 1;
count.set(c);
splitMenuButton.getItems().clear();
for (int choice = 1; choice <= 3; choice++) {
MenuItem mi = new MenuItem("Choice "+choice+" (" + c + ")");
splitMenuButton.getItems().add(mi);
}
}
});
BorderPane root = new BorderPane(null, splitMenuButton, null, null, null);
primaryStage.setScene(new Scene(root, 350, 150));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Sort of as an aside, I'm not sure this is a really good idea. Database connections are typically long-running processes (i.e. long enough to be visually noticeable in a UI environment). If you run this on the FX Application Thread, then you're going to block the UI from doing anything while the data is retrieved, and that's also right at the moment the user has just tried to do something. Of course, if you run it as a background task, then the menu will popup with the previous data, and then later update once the data is downloaded. I would recommend finding a way to populate this before the user requests it.
The 'arrow' is just another button used to show the popup with the menu items.
One easy way of knowing if this arrow button is pressed is by listening to the showing property of the popup.
Once you know that the popup is showing up, you can add your items.
#Override
public void start(Stage primaryStage) {
SplitMenuButton m = new SplitMenuButton();
m.showingProperty().addListener((ov,b,b1)->{
if(b1){
System.out.println("popup visible");
MenuItem menuItem = new MenuItem("New Option");
if(m.getItems().stream().noneMatch(i->i.getText().equals(menuItem.getText()))){
menuItem.setOnAction(e -> System.out.println("New Option added"));
m.getItems().add(menuItem);
}
}
});
m.setText("Click the arrow ->");
m.getItems().add(new MenuItem("First option"));
StackPane root = new StackPane(m);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
I would like to create a simple JavaFx class that shows the user a translucent rectangle (say an arbitrary 50% transparency) covering the users screen. It should simply allow me to get the Point of a mouse click event. This sounds trivial, but when I create transparent windows they always seem to be transparent to mouse events rather than just my requirement of semi-transparent visibility. The mouse event is never triggered.
I've used setMouseTransparent(false) on the rectangle and the root pane, but this makes no difference. I'd be really grateful if somebody could indicate any errors/misconceptions.
Here's the trivial test class I have created:
public class ClickScreen implements MouseListener {
private ClickScreenListener listener;
private Stage window;
private Point point;
public ClickScreen(ClickScreenListener listener) {
// Get screen size
Rectangle2D r = Screen.getPrimary().getBounds();
// Something to put stuff in
StackPane root = new StackPane();
// Translucent rectangle on the pane
Rectangle rectangle = new Rectangle(r.getWidth(), r.getHeight());
rectangle.setFill(Color.rgb(183, 183, 183, 0.5));
root.getChildren().add(rectangle);
Scene scene = new Scene(root, r.getWidth(), r.getHeight());
scene.setFill(null);
window = new Stage();
window.initStyle(StageStyle.TRANSPARENT);
window.setTitle("Click drop location");
window.setScene(scene);
this.listener = listener;
}
public Point getLocation(){
return point;
}
#Override
public void mouseClicked(MouseEvent e) {
point = e.getLocationOnScreen();
listener.screenClicked(point);
}
}
Edit:
A simpler example of the transparency issue I am experiencing is from this Hello World! example. When I mouse over the button, it's about 50:50 chance of clicking the button or just clicking "through" and giving focus to the underlying window (which is usually eclipse in my case). Would love you thoughts on this.
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
scene.setFill(null);
primaryStage.setScene(scene);
primaryStage.initStyle(StageStyle.TRANSPARENT);
primaryStage.show();
}
}
Check your Imports
You are using some kind of weird setup where you are mixing AWT/Swing classes and JavaFX classes, which really isn't advised (and doesn't work at all in the combination and manner you have used). Just be careful in your JavaFX programs not to import any java.awt.* or javax.swing.* classes unless you really know what you are doing in mixing code for two different toolkits.
Sample Solution
Here is a sample solution which imports only JavaFX classes and utilizes JavaFX events, but otherwise tries to stick to the coding/callback style of the sample code in your question. (The sample could be further simplified through use of Java 8 lambdas).
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.*;
public class ClickListenerSample
extends Application
implements ClickScreenListener {
private Label clickFeedbackLabel = new Label("");
#Override public void start(Stage stage) {
Button listen = new Button("listen");
listen.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
new ClickScreen(ClickListenerSample.this);
}
});
VBox layout = new VBox(10);
layout.getChildren().setAll(
listen,
clickFeedbackLabel
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout, 100, 80));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#Override public void screenClicked(Point2D point) {
clickFeedbackLabel.setText(point.getX() + ", " + point.getY());
}
}
interface ClickScreenListener {
void screenClicked(Point2D point);
}
class ClickScreen {
private ClickScreenListener listener;
private Stage window;
private Point2D point;
public ClickScreen(ClickScreenListener listener) {
// Get screen size
Rectangle2D r = Screen.getPrimary().getBounds();
// Something to put stuff in
StackPane root = new StackPane();
root.setStyle("-fx-background-color: null;");
// Translucent rectangle on the pane
Rectangle rectangle = new Rectangle(r.getWidth(), r.getHeight());
rectangle.setFill(Color.rgb(183, 183, 183, 0.5));
root.getChildren().add(rectangle);
Scene scene = new Scene(root, r.getWidth(), r.getHeight());
scene.setFill(null);
window = new Stage();
window.initStyle(StageStyle.TRANSPARENT);
window.setTitle("Click drop location");
window.setScene(scene);
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
point = new Point2D(event.getScreenX(), event.getScreenY());
listener.screenClicked(point);
window.hide();
}
});
window.show();
this.listener = listener;
}
public Point2D getLocation(){
return point;
}
}
In the code below I have a ToolBar and I add buttons of various sizes to it. I would like the buttons to be square and all the same size. So basically find the longest width or height from all the buttons, and set all other button widths and height to this size. However, the buttons can change size so I require a binding I think. I can't quite figure it out - anyone know how to do it?
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ToolBarButtonTest extends Application {
#Override
public void start(Stage stage) throws Exception {
BorderPane borderPane = new BorderPane();
Scene scene = new Scene(borderPane, 500, 500);
ToolBar toolBar = new ToolBar();
Button button1 = new Button("s");
Button button2 = new Button("ss");
Button button3 = new Button("sss");
toolBar.getItems().addAll(button1, button2, button3);
borderPane.setTop(toolBar);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Thanks, Nick.
Here you go. This example will use CSS to turn the button shape into a square, and install a validation listener to track layout changes to the buttons and update the widths accordingly.
public class SquareButtons extends Application {
#Override
public void start(final Stage stage) throws Exception {
/* Assume these are your toolbar elements */
final Button[] buttons = new Button[]{
new Button("Short"),
new Button("Slightly longer"),
new Button("Very very very long button")
};
/* This would, of course, belong in a style sheet - it turns the buttons square */
for (Button b : buttons)
b.setStyle("-fx-background-radius: 0");
/* This will set the pref width/height of all your buttons to the maximum of the pref width/height of the larget one */
final InvalidationListener listener = new InvalidationListener() {
public void invalidated(final Observable observable) {
double size = 0;
for (Button b : buttons) {
size = Math.max(size, b.prefWidth(Integer.MAX_VALUE));
size = Math.max(size, b.prefHeight(Integer.MAX_VALUE));
}
for (Button b : buttons) {
b.setPrefWidth(size);
b.setPrefHeight(size);
}
}
};
for (Button b : buttons)
b.widthProperty().addListener(listener);
final ToolBar toolbar = new ToolBar();
toolbar.getItems().addAll(buttons);
final Scene scene = new Scene(toolbar);
stage.setScene(scene);
stage.setWidth(800);
stage.setHeight(200);
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Update: Didn't read question thoroughly enough, see comment.
Since no one else has responded, I'll pass along a partial solution. I'm a noob at JavaFX, but since the parent is responsible for laying out the child nodes, I would derive a modified version of the ToolBar class and override some of the layout code. Here's is a modification of your example that works, but is a bit kludgy.
package toolbarbuttontest;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ToolBarButtonTest extends Application {
#Override
public void start(Stage stage) {
BorderPane borderPane = new BorderPane();
Scene scene = new Scene(borderPane, 500, 500);
Button btn1 = new Button("short");
Button btn2 = new Button("between");
Button btn3 = new Button("pretty long");
SquareButtonToolBar toolBar = new SquareButtonToolBar();
toolBar.getItems().addAll(btn1, btn2, btn3);
borderPane.setTop(toolBar);
stage.setScene(scene);
stage.show();
toolBar.requestLayout();
}
// A derivative of the ToolBar class that will resize all buttons to be
// the same size (based on the length of the longest button label) and
// square.
class SquareButtonToolBar extends ToolBar {
#Override
protected void layoutChildren() {
double minPrefSize = calculatePrefChildSize();
for (Node n : getItems()) {
if (n instanceof Button) {
((Button) n).setPrefWidth(minPrefSize);
((Button) n).setPrefHeight(minPrefSize);
}
}
super.layoutChildren();
}
private double calculatePrefChildSize() {
double minPrefSize = 0.0d;
for (Node n : getItems()) {
if (n instanceof Button) {
minPrefSize = Math.max(minPrefSize, n.prefWidth(-1));
}
}
return minPrefSize;
}
}
public static void main(String[] args) {
launch(args);
}
}
The SquareButtonToolBar class overrides the layout code by setting the preferred width and height of the buttons to the length of the longest button, thus making all the buttons square and the same size. The kludge is the call to toolBar.requestLayout() at the bottom of the start method. Without this call, even though the button widths are all shown as desired on program start up, the heights are not displayed correctly until the window is re-sized. Not sure what I'm missing myself. If you find the answer, please post an update.
UPDATE: Modified example to a fully working one, but with a nasty kludge.