EDIT: The problems seem to emerge from the use of a SplitPane. I've removed that, but now I'm encountering MouseTransparent issues, cf comments
I'm encountering a problem on a StackPane, where a Controller class is supposed to manage node translations via mouse dragging:
For some reason, the .setOnMouseEntered event correctly fires whenever the mouse enters the StackPane. However, setOnMousePressed (when clicking on a node in the StackPane) doesn't fire at all, except on a label in the center of the StackPane. When I remove the label, nothing fires. The StackPane itself is comprised of a SplitPane, which is comprised of several other nodes.
Some of those nodes (the ones supposed to be dragged around) also have some setOnMousePressed, etc event management, which gets fired correctly !
To understand the context a bit more: I am working on a Naval Battle game implementation. I am currently trying to manage the boat placement via a controller class (so that I can verify that a boat doesn't get placed out of bounds or on another boat). The controller class (BoatMoveManager), when instancied, creates all these mouseEvent handlers on the highest StackPane (GameBoard extends StackPane).
public class BoatMoveManager {
GameBoard gB;
public BoatMoveManager(GameBoard g){
gB = g;
//the following mouseEventHandler fires correctly whenever the mouse enters the GameBoard (StackPane)
gB.setOnMouseEntered(event -> {
System.out.println("GameBoard OnMouseEntered");
//the following mouseEventHandler doesn't fire, except on a label in the middle of the GameBoard
gB.setOnMousePressed(event -> {
System.out.println("GameBoard OnMousePressed");
});
I don't understand why the event fires with the label, and not with the rest of the nodes in the GameBoard... (It might be the fact that the label is directly "inside" the GameBoard, while other nodes are in the top part of a horizontal splitter ?)
For example, here is the constructor for the GameBoard:
public class GameBoard extends StackPane {
private SplitPane splitter;
private TilePane gameZone;
private TabZone tabs;
private Grid playerGrid, opponentGrid, playerHarbor, opponentHarbor;
private FlowPane playerGrids, opponentGrids, playerZone, opponentZone;
private Score playerScore, opponentScore;
private Label popup;
private static HBox playerHBox, opponentHBox;
private static VBox playerVBox, opponentVBox;
private static BoatMoveManager bMM;
private List<Boat> playerBoatsList = new ArrayList<Boat>(), opponentBoatsList = new ArrayList<Boat>() ;
public GameBoard(){
super();
playerGrid = new Grid('G','H');
opponentGrid = new Grid('G','H');
playerHarbor = new Grid('H','H');
opponentHarbor = new Grid('H','H');
bMM = new BoatMoveManager(this);
for(int i = 1 ; i < 5 ; ++i){
playerBoatsList.add(new Boat(i, playerGrid, playerHarbor));
opponentBoatsList.add(new Boat(i, opponentHarbor, opponentGrid));
}
playerGrid.drawBoat(playerBoatsList.get(0), 3, 4);
playerGrid.drawBoat(playerBoatsList.get(1), 3, 5);
playerGrid.drawBoat(playerBoatsList.get(2), 3, 6);
playerGrid.drawBoat(playerBoatsList.get(3), 3, 7);
playerScore = new Score();
opponentScore = new Score();
gameZone = new TilePane(Orientation.HORIZONTAL);
gameZone.setStyle("-fx-background-color: lightgray");
playerHBox = new HBox();
playerHBox.getChildren().addAll(playerHarbor, playerGrid);
playerHBox.setSpacing(Grid.getUnit());
playerVBox = new VBox();
playerVBox.getChildren().addAll(playerScore,playerHBox);
playerVBox.maxWidthProperty().bind(gameZone.widthProperty().divide(2));
playerScore.setAlignment(Pos.CENTER);
opponentHBox = new HBox();
opponentHBox.getChildren().addAll(opponentGrid, opponentHarbor);
opponentHBox.setSpacing(Grid.getUnit());
opponentVBox = new VBox();
opponentVBox.getChildren().addAll(opponentScore,opponentHBox);
opponentScore.setAlignment(Pos.CENTER);
gameZone.getChildren().addAll(playerVBox, opponentVBox);
gameZone.setHgap(Grid.getUnit()*3);
gameZone.setAlignment(Pos.CENTER);
gameZone.setSnapToPixel(false);
tabs = new TabZone();
splitter = new SplitPane();
splitter.setOrientation(Orientation.VERTICAL);
splitter.setDividerPositions(0.75f,0.25f);
splitter.getItems().addAll(gameZone,tabs);
splitter.setOnDragEntered(event ->{
BoatMoveManager.setTarget(splitter);
});
popup = new Label("uninitialized");
this.getChildren().addAll(splitter,popup);
this.setOnDragEntered(event -> {
BoatMoveManager.setTarget(this);
});
}
The label I'm talking about is "popup", which is added at the very end of the constructor along with "splitter" in
this.getChildren().addAll(splitter,popup);
but take in account the fact that even when I don't add it, events like MousePressed don't fire through GameBoard...
Here's a layout of the GameBoard to clear things up (grids and boats layout is self explanatory):
(Open this image in a new tab to view it in full size)
I hope this isn't too cluttered to get a sense of what I'm trying to do.. And why it doesn't work.
I'm really not sure at this point. I'd pretty much have to test the code on my computer to know for sure but try
this.setMouseTransparency(true);
This will make it to where the stackpane itself can't be clicked on. Also for future reference you can drop the "this" at the beginning of your calls for GameBoard. It's perfectly legal to call getChildren().add(node) if your class extends StackPane.
After running into the same problem (again) ..
I believe a StackPane is your problem. They do not like to pass event aparently..
In my case, setting :
stackPane.setPickOnBounds(false);
was my fix.
I hope this resolves the Issue for you.. perhaps a bug?
Related
I want to know if there's a simple way to link the functionality of two nodes in different stages in JavaFX.
In one stage, I have a Menu containing two radioMenuItem's in a ToggleGroup together which control the theme of the app:
BorderPane mainPane = new BorderPane();
MenuBar menuBar = new MenuBar();
menuBar.setStyle("-fx-base: white");
Menu options = new Menu("Options");
MenuItem appearance = new MenuItem("Appearance");
Menu themes = new Menu("Theme");
ToggleGroup toggleGroup = new ToggleGroup();
RadioMenuItem dark = new RadioMenuItem("Dark");
dark.setToggleGroup(toggleGroup);
RadioMenuItem light = new RadioMenuItem("Light");
light.setToggleGroup(toggleGroup);
themes.getItems().addAll(dark, light);
options.getItems().addAll(appearance, themes);
menuBar.getMenus().addAll(options);
mainPane.setTop(menuBar);
This pane belongs to the Stage that opens when the app launches. When a new window opens, I want the user to be able to control the theme from that window as well. I tried adding the same items to the Pane in that Stage, but then they get removed from the Pane in this one. I don't want to create new objects for every window, since they would be exactly the same, just in a different Pane and Stage.
A node can only be attached to a single parent at any given time.
From the Node javadoc:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in all of the following: as the root node of a Scene, the children ObservableList of a Parent, or as the clip of a Node.
If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent. If a program attempts to modify the scene graph in any other way that violates the above rules, an exception is thrown, the modification attempt is ignored and the scene graph is restored to its previous state.
It is possible to rearrange the structure of the scene graph, for example, to move a subtree from one location in the scene graph to another. In order to do this, one would normally remove the subtree from its old location before inserting it at the new location. However, the subtree will be automatically removed as described above if the application doesn't explicitly remove it.
What you wish to do, simultaneously have the same node attached to two different scenes, is impossible by design.
This question is really two questions, so I will provide two answers.
The first question is the question from the question title, which is whether on not you can place the same node in different scenes?
you can't, as answered in my other answer.
The second question is that, given that you can't place the same node in different stages, how do you synchronize the state of the application across multiple stages and views?
you use a shared model as explained in this answer.
General Approach
The answer is to apply MVC principles, similar to those outlined in:
Applying MVC With JavaFx
With this approach, you have a shared model class. Each view in the UI listens to the shared model and can update its view state when a change in the model is detected. The views can also update the model, which will trigger updates on other views of the model.
Specific Theme Example
The rest of the answer provides a concrete example of the application of a shared model state for an application-wide theme style that can be modified from multiple different UI elements.
The solution shows two windows, each with its own separate menu controls for the application:
A File menu which allows the user to quit the application.
A Theme menu which allows the user to select between a Light or a Dark theme.
The theme selection will immediately apply across all windows in the application and all radio menu items in the application will be updated to reflect the new theme. This is accomplished through a shared Theme model class with a listenable selected theme property. When the user selects a new theme, the shared selected theme property is updated to the new value. Listeners on the theme property are then fired to update the UI state and theme as appropriate.
Menu items in macs can be system menu items disassociated with the stage. So, there is a line in the solution which can be uncommented if you want to use the shared system menu on a Mac (recommended).
The solution could be made more abstract and configurable to deal with additional themes or to work via interfaces to perform tasks such as registering listeners and invoking callbacks. For example, subclassable ThemedStages or a decoration pattern could be provided that shares themeable logic, or a shared ThemeManager could be created to act as a controller to register and unregister themed stages and apply theme changes to them. But, for this example, I tried not to make it too abstract. Hopefully, that allows it to be easy to understand.
The example also inlines the CSS for convenience, but you can have the themes in separate stylesheets if desired.
Sometimes you can use binding to help support the interface of the model with the UI components. But here, there is not a one-to-one match between the model and the UI control properties, so listeners are used instead.
Stage menus light theme
Stage menus dark theme
Mac system menu light theme
Mac system menu dark theme
Example Code
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ThemedMultiStageApp extends Application {
#Override
public void start(Stage stage) throws Exception {
Theme theme = new Theme();
StageOne stageOne = new StageOne(theme);
stageOne.show();
StageTwo stageTwo = new StageTwo(theme);
stageTwo.setX(stageOne.getX() + stageOne.getWidth() + 20);
stageTwo.setY(stageOne.getY());
stageTwo.show();
}
public static void main(String[] args) {
launch(args);
}
}
class StageOne extends Stage {
public StageOne(Theme theme) {
StackPane view = new StackPane(
new Button("Button One")
);
BorderPane layout = new BorderPane();
layout.setTop(new AppMenuBar(theme));
layout.setCenter(view);
Scene scene = new Scene(
layout, 200, 150
);
theme.applyTo(scene);
theme.themeProperty().addListener((observable, oldTheme, newValue) ->
theme.applyTo(scene)
);
setScene(scene);
}
}
class StageTwo extends Stage {
public StageTwo(Theme theme) {
StackPane view = new StackPane(
new Button("Button Two")
);
BorderPane layout = new BorderPane();
layout.setTop(new AppMenuBar(theme));
layout.setCenter(view);
Scene scene = new Scene(
layout, 200, 150
);
theme.applyTo(scene);
theme.themeProperty().addListener((observable, oldTheme, newValue) ->
theme.applyTo(scene)
);
setScene(scene);
}
}
class AppMenuBar extends MenuBar {
public AppMenuBar(Theme theme) {
// File menu
MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.setOnAction(e -> Platform.exit());
Menu fileMenu = new Menu(
"File", null,
exitMenuItem
);
// Theme selection menu.
ToggleGroup themeToggleGroup = new ToggleGroup();
RadioMenuItem lightThemeMenuItem = new RadioMenuItem("Light");
lightThemeMenuItem.setOnAction(e -> theme.setTheme(Theme.ThemeType.LIGHT));
lightThemeMenuItem.setToggleGroup(themeToggleGroup);
RadioMenuItem darkThemeMenuItem = new RadioMenuItem("Dark");
darkThemeMenuItem.setOnAction(e -> theme.setTheme(Theme.ThemeType.DARK));
darkThemeMenuItem.setToggleGroup(themeToggleGroup);
setThemeToggle(
theme.getTheme(), themeToggleGroup, lightThemeMenuItem, darkThemeMenuItem
);
theme.themeProperty().addListener((observable, oldTheme, newTheme) ->
setThemeToggle(
newTheme, themeToggleGroup, lightThemeMenuItem, darkThemeMenuItem
)
);
Menu themeMenu = new Menu("Theme");
themeMenu.getItems().addAll(lightThemeMenuItem, darkThemeMenuItem);
// If you want to use the system menu bar on a Mac (recommended)
// then uncomment this line.
// Note, the useSystemMenuBar setting will have no effect on a
// platforms which do not support system menu bars (e.g. Windows).
// setUseSystemMenuBar(true);
getMenus().setAll(
fileMenu, themeMenu
);
setMinSize(MenuBar.USE_PREF_SIZE, MenuBar.USE_PREF_SIZE);
}
private void setThemeToggle(Theme.ThemeType newTheme, ToggleGroup themeToggleGroup, RadioMenuItem lightThemeMenuItem, RadioMenuItem darkThemeMenuItem) {
switch(newTheme) {
case LIGHT -> themeToggleGroup.selectToggle(lightThemeMenuItem);
case DARK -> themeToggleGroup.selectToggle(darkThemeMenuItem);
}
}
}
class Theme {
public enum ThemeType {
LIGHT, DARK
}
private static final String INDIA_INK = "#383d48";
private static final String CSS_TEMPLATE =
"""
data:text/css,
.root {
-fx-font-size: 16px;
-fx-base: %s;
}
""";
private static final String DARK_CSS =
CSS_TEMPLATE.formatted(INDIA_INK);
private static final String LIGHT_CSS =
CSS_TEMPLATE.formatted("antiquewhite");
private static final String[] THEME_STYLESHEETS = { LIGHT_CSS, DARK_CSS };
private final ObjectProperty<ThemeType> theme = new SimpleObjectProperty<>(ThemeType.LIGHT);
public ThemeType getTheme() {
return theme.get();
}
public ObjectProperty<ThemeType> themeProperty() {
return theme;
}
public void setTheme(ThemeType theme) {
this.theme.set(theme);
}
public void applyTo(Scene scene) {
scene.getStylesheets().removeAll(THEME_STYLESHEETS);
scene.getStylesheets().add(
switch (theme.get()) {
case LIGHT -> LIGHT_CSS;
case DARK -> DARK_CSS;
}
);
}
}
The title may be a bit vague, so allow me to define it a little better. I have a working piece of code (down below): a simple main menu for a game I am working on. Everything works well, except for the Start button.
What I want to be able to do is click the Start button, and have a new scene appear on the same stage (window). I do not want to see a new window open. I have talked with someone more experienced in Java, and they told me to create separate classes for the MenuFX and the GameFX. If that is the case, I would need to call some start or launch method on the GameFX class from the MenuFX class, correct? Is this the best approach, or would I want to keep all FX-related code in one class? Also, I should keep the same stage for all FX work, no?
This post shed some light on things, but I am not well-versed in some of the terms discussed- for instance, I still do not understand the concept of Root.
Also, this post talks about a similar application, but I am not using FXML or SceneBuilder... I do not know if any of it is relatable.
MenuFX.java - I have removed some of the working code, simply for brevity. You can see that all I need help with is tying the Start button to some functionality that makes a new empty scene.
/*
* This is simply working on the title screen.
*/
// Asssume all imports are correct
import java.everythingNeeded
public class MenuFX extends Application {
#Override
public void start (Stage primaryStage) {
// Make the window a set size...
primaryStage.setResizable(false);
// Create menu vbox and set the background image
VBox menuVBox = new VBox(30);
menuVBox.setBackground(new Background(new BackgroundImage(new
Image("image/bambooBG.jpg"), null, null, null, new BackgroundSize(45,
45, true, true, true, true))));
// Create start button
Button startButton = new Button("Start Game");
// TODO Some things...
// Need assistance here
// Create help button
Button helpButton = new Button("Help");
helpButton.setOnAction(e -> THINGS);
// Create music toggle button
ToggleButton musicButton = new ToggleButton("Music On/Off");
musicButton.setOnAction(e -> THINGS);
// Create credits button
Button creditsButton = new Button("Credits");
creditsButton.setOnAction(THINGS);
// Create exit button and set it to close the program when clicked
Button endButton = new Button("Exit Game");
endButton.setOnAction(e -> Platform.exit());
// Add all nodes to the vbox pane and center it all
// Must be in order from top to bottom
menuVBox.getChildren().addAll(startButton, helpButton, musicButton, creditsButton, endButton);
menuVBox.setAlignment(Pos.CENTER);
// New scene, place pane in it
Scene scene = new Scene(menuVBox, 630, 730);
// Place scene in stage
primaryStage.setTitle("-tiles-");
primaryStage.setScene(scene);
primaryStage.show();
}
// Needed to run JavaFX w/o the use of the command line
public static void main(String[] args) {
launch(args);
}
}
Restating: I want to click the Start button and have the currently open window change to an empty scene.
Here is a pastebin of the MenuFX class in its entirety:
http://pastebin.com/n6XbQfhc
Thank you for any help,
Bagger
The basic idea here is you would do something like:
public class GameFX {
private final BorderPane rootPane ; // or any other kind of pane, or Group...
public GameFX() {
rootPane = new BorderPane();
// build UI, register event handlers, etc etc
}
public Pane getRootPane() {
return rootPane ;
}
// other methods you may need to access, etc...
}
Now back in the MenuFX class you would do
Button startButton = new Button("Start Game");
startButton.setOnAction(e -> {
GameFX game = new GameFX();
primaryStage.getScene().setRoot(game.getRootPane());
});
After a lot of research I only could get that board image with using label. Now I cannot change it's position. I tried a lot of functions. What is the exact function do I need
public class Board extends JFrame {
private ImageIcon image;
private JLabel label;
public Board() {
image = new ImageIcon(getClass().getResource("board.png"));
label = new JLabel(image);
label.setLocation(200, 0); //This is the one that I expected to do the thing
add(label);
}
public static void main(String [] args) {
Board b = new Board();
b.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
b.setVisible(true);
b.setSize(1280, 1000);
}
}
Don't try to manually set the location of a component. Swing was designed to be used with layout managers. Read the section from the Swing tutorial on Using Layout Managers for more information.
One way to position a component is to give the component a Border. So you could do something like:
label.setBorder( new EmptyBorder(200, 0, 0, 0) );
The tutorial also has a section on How to Use Borders.
First of all: you shouldn't move components manually. This should be left to the layoutmanager. But you can. The basic problem you're facing is - or atleast i think so - : you're board still has an layoutmanager set, which will continue to layout the board and due to this aswell move (and handle the size of) you're component. Just call setLayout(null); before positioning the label and specify the size and it should work.
There is plenty of online resource on how to add a graphic to a JavaFX button.
However I would like to know if there is a way to add a second (or any number, but more than 2 images probably woudn't make much sense in most situations) image.
My use case : i have a button with a square icon on the left, followed by a text label. The image is a representation of some real-life concept that button is linked with (could e.g. a car or a person). I would like to add a small icon to the right of some buttons, a "right chevron" to indicate the nature of the interaction.
I was thinking maybe to use a HBox with the full width as the graphic node of the button, and add the 2 images to it, but I don't think it is possible to put the text on top of the graphic node.
Any idea ?
Create your own custom node with icons and text and set it as graphic. Of course you don't show the button text because it's already in your custom graphic node.
Here's a simple example. You need to provide the files icon1.png and icon2.png.
public class ButtonWithMultipleIcons extends Application {
#Override
public void start(Stage primaryStage) {
try {
Group group = new Group();
Button button = new Button();
button.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
HBox hBox = new HBox();
ImageView icon1 = new ImageView( getClass().getResource( "icon1.png").toExternalForm());
ImageView icon2 = new ImageView( getClass().getResource( "icon2.png").toExternalForm());
Label label = new Label("Text");
//make the button grow if you want the right icon to always be on the right of the button :
label.setMaxWidth(Long.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
hBox.getChildren().addAll( icon1, label, icon2);
button.setGraphic(hBox);
group.getChildren().add( button);
Scene scene = new Scene(group,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
You can try using html formatting in your button and adding an image with the tag before and after your text.
You can find more information about it in here:http://docs.oracle.com/javase/tutorial/uiswing/components/html.html
I had the same thing to get along with. You describing the solution with using a HBox. I think this would also be the best way.
Do the following:
Create a new CustomButton with your own specific layout maybe using a HBox and extending Button class from JavaFX. Style it your way you need it and you can add as many images you want to have in your custom button. Maybe it can look like this:
public class CustomButton extends Button
{
private HBox hbox;
private ImageView image1;
private ImageView image2;
public CustomButton()
{
super();
// Here add your specific images to global or local variables and then add all the children (images) to your HBox layout and this one to the button.
}
}
The problem ist, that JavaFX provides normal buttons and not such deeply customized components. If you want to style it, you can use CSS anyway.
I am trying to implement a custom tooltip using the javafx.stage.Popup. The sample demo code is:
public class PopupDemo extends Application {
private Popup tooltip;
private final SepiaTone sepiaTone = new SepiaTone();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("PopupDemo");
Label content = new Label();
content.setStyle("-fx-background-color:#FCFBBD; -fx-padding: 5; -fx-border-color: #BFBD3B");
tooltip = new Popup();
tooltip.getContent().add(content);
VBox vbox = new VBox(10);
for (int i = 0; i < 5; i++) {
final Label lbl = new Label("item " + i);
lbl.setStyle("-fx-border-color:darkgray; -fx-background-color:lightgray");
lbl.setMaxSize(80, 60);
lbl.setMinSize(80, 60);
lbl.setAlignment(Pos.CENTER);
lbl.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(final MouseEvent e) {
lbl.setEffect(sepiaTone);
lbl.setStyle("-fx-cursor: hand");
Label content = (Label) tooltip.getContent().get(0);
content.setText(lbl.getText());
tooltip.show(lbl, e.getScreenX(), e.getScreenY());
}
});
lbl.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
lbl.setEffect(null);
lbl.setStyle("-fx-cursor: default");
tooltip.hide();
}
});
vbox.getChildren().add(lbl);
}
StackPane root = new StackPane();
root.setPadding(new Insets(20));
root.getChildren().add(vbox);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
}
When I move the mouse over the labels the popup shows up and it is working great. But in some cases the two mouse event handlers OnMouseEntered and OnMouseExited are being called continuously one after another. One can reproduce this by running provided example, maximising a window and hovering labels continuously.
Is there a way to avoid this? I'm using JavaFX 2.0.1. Thanks.
It's a classic problem: you put mouse at a point, node receives MouseEntered — tooltip appears under the mouse and covers the node triggering MouseExited.
To avoid that you can change tooltip.show(lbl, e.getScreenX(), e.getScreenY()) call to
tooltip.show(lbl, e.getScreenX() + 1, e.getScreenY() + 1);
This is not really an answer, so much as pointers to things you might try or investigate further.
You could look at the implementation of Tooltip Skin and Behavior to see how it handles some of these cases.
The easiest way to implement a custom popup is just to use a Tooltip, style it the way you need using css and use the Tooltip's setGraphic method to add any custom Node content you want.
If you prefer to use your own implementation, I think you need to keep track of whether the popup has been displayed or not, so you don't try to show it if it is already showing, etc. You may also need invoke the hiding of the popup by having a mouse exit handler installed on the popup itself. You also might want a click to dismiss function for the popup by implementing a mouse click handler on the popup. You should also consider whether you should do a straight subclass of Popup or PopupControl, though using Popup as you have is likely more straightforward.