i have 2 fxml
FXML A:
it contains borderpane with ID fx:id="UnitBorderPane"
FXML B:
it contains anchorpane with ID fx:id="UnitForm"
i load the "FXML B" at borderpane A on the left side
FXMLLoader loader = new
FXMLLoader(getClass().getResource("/projectname/unit/UnitForm.fxml"));
Pane pane = (Pane) loader.load();
UnitBorderPane.setLeft(pane);
it is kind of fxml form, so we have a button with action
<Button layoutX="102.0" layoutY="169.0" mnemonicParsing="false" onAction="#saveUnit" text="Save" />
how to hide that FXML A BorderPane left?
#FXML
private void saveUnit(ActionEvent event) {
BorderPane borpane = (BorderPane)UnitForm.getParent().lookup("#UnitBorderPane");
borpane.setLeft(null);
}
this code not work, the borpane variable is null so i cannot set the borderPane FXML A Left to null.
I think it should just be
BorderPane borpane = (BorderPane)UnitForm.getParent();
However, none of this feels very robust; for example if you decide to change the layout structure at all you will likely have a lot of code to change in various classes. I would add a property to the controller for UnitForm.fxml that you can observe from the controller for UnitBorderPane. Something like:
public class UnitFormController { // your actual class name may differ....
private final BooleanProperty saved = new SimpleBooleanProperty();
public BooleanProperty savedProperty() {
return saved ;
}
public final boolean isSaved() {
return savedProperty().get();
}
public final void setSaved(boolean saved) {
savedProperty().set(saved);
}
// other code as you already have...
#FXML
private void saveUnit() {
setSaved(true);
}
// ...
}
Then you do
FXMLLoader loader =
new FXMLLoader(getClass().getResource("/projectname/unit/UnitForm.fxml"));
Pane pane = (Pane) loader.load();
UnitFormController controller = loader.getController();
controller.savedProperty().addListener((obs, wasSaved, isNowSaved) -> {
if (isNowSaved) {
UnitBorderPane.setLeft(null);
}
});
UnitBorderPane.setLeft(pane);
Now the management of the UnitBorderPane is all in one place, instead of being split over two controllers, and there are no lookups (which are not robust). The controller for the UnitForm just sets a property and lets the other controller respond as it sees fit.
Related
Im working on a small JavaFX project. In one of my scenes, I want to dynamically add and remove a custom component I implemented, which extends from TitledPane, to respectively from an Accordion. This works all well and good, but after removing the pane from the Accordion the damn thing won't resize immidately, but only after I click somewhere on the gui. I prepared the following GIF to visualize the problem to you.
Can someone tell me why the accordion only resizes after I clicked somewhere on the gui interface and not immidiately? I mean auto resizing doesn't seem to be a problem, it just won't trigger...can someone tell me why that is the case? Maybe this is obvious, but I am not very familiar with JavaFX, so I am really stuck here. I also observed a similar behavior with other component, so maybe I am missing something fundamentally here.
UPDATE
Ok I created a minimal example for you to reproduce my problem. You can clone the repository on GitHub javafx-demo and try it out yourself. Doing this I noticed, that the Accordion resizes only if I click on it and not when I click anywhere else on the gui.
UPDATE 1
I simplified the example further. You can find the example in the GitHub repository above or see the code below:
App
public class App extends Application {
#Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource("parentView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
ParentController
public class ParentController {
#FXML
private Accordion accordion;
public void onAddAction() {
var itemControl = new ItemControl();
EventHandler<ActionEvent> removeEventHandler = event -> {
accordion.getPanes().remove(itemControl);
};
itemControl.setOnRemoveProperty(removeEventHandler);
accordion.getPanes().add(itemControl);
}
}
Parent View
<StackPane xmlns="http://javafx.com/javafx/16"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="org.example.ParentController">
<Group StackPane.alignment="CENTER">
<VBox>
<Accordion fx:id="accordion"/>
<Button onAction="#onAddAction" text="Add"/>
</VBox>
</Group>
</StackPane>
ItemControl
public class ItemControl extends TitledPane {
private final UUID id = UUID.randomUUID();
private final ObjectProperty<EventHandler<ActionEvent>> onRemoveProperty = new SimpleObjectProperty<>();
#FXML
private Button removeButton;
public ItemControl() {
FXMLLoader fxmlLoader = new FXMLLoader(ItemControl.class.getResource("itemControl.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
#FXML
public void initialize() {
removeButton.onActionProperty().bind(onRemoveProperty);
}
public void setOnRemoveProperty(EventHandler<ActionEvent> onRemoveProperty) {
this.onRemoveProperty.set(onRemoveProperty);
}
// equals and hashCode omitted for brevity (id instance variable is used as identifier)
}
ItemControl FXML
<fx:root type="javafx.scene.control.TitledPane" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<VBox>
<Button fx:id="removeButton" text="Remove"/>
</VBox>
</fx:root>
The behavior is a bug in AccordionSkin. The technical reason is that it keeps internal references to the current and previous expanded pane - both used in calculating the min/pref height - which are not updated correctly on removing the expanded pane. A fix would be to null those references if the panes are no longer part of the accordion, f.i. from the skin's listener to the panes list.
There is no clean way to work around this because all involved fields/methods are private - if we are allowed to go dirty, though, we can hack around the bug with reflection.
The basics:
extend AccordionSkin and let our accordion use the extended version
in the skin, override both computeMin/Pref/Height to check/fix the references before returning super
check: the panes should be contained in the accordion's panes
fix: if not, set the reference to null
Notes:
the reflective access to internal fields requires that the package is opened at runtime
the usual beware: tweaking/relying on implementation internals is highly version dependent and might/will break eventually
FXUtils is my local utility class for reflection, you have to replace it with your own implementation
The code:
public class SimpleLayoutAccordionOnRemove extends Application {
/**
* AccordionSkin that hacks the broken layout after remove of expanded pane.
*/
public static class HackedAccordionSkin extends AccordionSkin {
public HackedAccordionSkin(Accordion control) {
super(control);
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
private void checkPaneFields() {
checkPaneField("previousPane");
checkPaneField("expandedPane");
}
/**
* Check if the pane referenced by the field with the given name is contained
* in the accordion's panes and sets it to null if not.
*/
private void checkPaneField(String fieldName) {
TitledPane prev = (TitledPane) FXUtils.invokeGetFieldValue(AccordionSkin.class, this, fieldName);
if (!getSkinnable().getPanes().contains(prev)) {
FXUtils.invokeSetFieldValue(AccordionSkin.class, this, fieldName, null);
}
}
}
private Parent createContent() {
Accordion accordion = new Accordion() {
#Override
protected Skin<?> createDefaultSkin() {
return new HackedAccordionSkin(this);
}
};
Button add = new Button("add");
add.setOnAction(e -> {
addTitledPane(accordion);
});
VBox accBox = new VBox(accordion, add);
StackPane content = new StackPane(new Group(accBox));
return content;
}
int count;
private void addTitledPane(Accordion accordion) {
TitledPane pane = new TitledPane();
pane.setText("Pane " + count++);
Button remove = new Button("remove");
remove.setOnAction(e -> {
accordion.getPanes().remove(pane);
});
VBox box = new VBox(remove);
pane.setContent(box);
accordion.getPanes().add(pane);
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 600, 400));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I'm just a complete beginner when it comes to programming or java.
So for the start my plan was to create a window useing JavaFX(combined with scene builder) where I do have a button that leads me to another window where i do have a combobox. I googled for hours now to find a way to fill that combobox with choices but all the solutions i found don't work for me. Thats why I think I made some mistakes here and I hope you can somehow help me. Or at list give me a hint what I should learn/read to get to the solution myself.
So to start with, here's my main.java code where I build my first stage.
main.java:
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Parent root= FXMLLoader.load(getClass().getResource("Scene-Hauptmenu.fxml"));
primaryStage.setTitle("Fishbase");
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root));
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
In my "Scene-Hauptmenu.fxml" all that matters is the button that leads me to my second window:
Scene-Hauptmenu.fxml:
<Button id="btn_gefangen" fx:id="btn_gefangen" mnemonicParsing="false" onAction="#gefangen" text="Ich habe Fische gefangen!" GridPane.rowIndex="1" />
So far everything works fine and I can switch to my second window without a problem. But I think my main problem lies within my controller class so here it is.
MyController.java:
public class MyController implements Initializable{
private Node node;
private Stage stage;
private Scene scene;
private FXMLLoader fxmlLoader;
private Parent root;
#FXML
private Button btn_gefangen;
#FXML
private ComboBox<String> chobo_fisch;
#FXML
private Button btn_gefangen_zurueck;
#Override
public void initialize(URL location, ResourceBundle resources) {
}
public void gefangen(ActionEvent event) throws IOException{
node = (Node) event.getSource();
stage = (Stage) node.getScene().getWindow();
scene = stage.getScene();
fxmlLoader = new FXMLLoader (getClass().getResource("gefangen.fxml"));
root = (Parent) fxmlLoader.load();
scene.setRoot(root);
stage.sizeToScene();
stage.setTitle("Fische eintragen");
}
public void gefangen_zurueck(ActionEvent event) throws IOException{
node = (Node) event.getSource();
stage = (Stage) node.getScene().getWindow();
scene = stage.getScene();
fxmlLoader = new FXMLLoader (getClass().getResource("Scene-Hauptmenu.fxml"));
root = (Parent) fxmlLoader.load();
scene.setRoot(root);
stage.sizeToScene();
stage.setTitle("Fishbase");
}
}
So the button "btn_gefangen" leads me to that other window where i do have the combobox with the fx:id "chobo_fisch".
gefangen.fxml:
<ComboBox fx:id="chobo_Fisch" prefWidth="150.0"/>
So I googled for hours but I still didnt find any solution to fill the combobox with choices that works with my code. What did I do wrong? Can anyone help me here?
Best regards
Jannik
I found three variants, depending on your setup:
1st variant
// Weekdays
String week_days[] =
{ "Monday", "Tuesday", "Wednesday",
"Thrusday", "Friday" };
// Create a combo box
ComboBox combo_box = new ComboBox(FXCollections.observableArrayList(week_days));
(Soure: https://www.geeksforgeeks.org/javafx-combobox-with-examples/)
2nd variant
final ComboBox emailComboBox = new ComboBox();
emailComboBox.getItems().addAll(
"jacob.smith#example.com",
"isabella.johnson#example.com",
"ethan.williams#example.com",
"emma.jones#example.com",
"michael.brown#example.com"
);
Source: (https://docs.oracle.com/javafx/2/ui_controls/combo-box.htm)
3rd variant (for FXML)
<ComboBox fx:id="someName">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="1"/>
<String fx:value="2"/>
<String fx:value="3"/>
<String fx:value="4"/>
</FXCollections>
</items>
<value>
<String fx:value="1"/>
</value>
</ComboBox>
Edit
As mentioned by fabian you should make sure to include the FXML imports:
<?import javafx.collections.FXCollections?>
<?import java.lang.String?>
The second one may not be needed.
I'm new to those stuff but I think this is how it should look or at least close too if I understood what you wanted.
Example below:
ComboBox<String> stuff = new ComboBox<>();
stuff.getItems().addAll("1","2","5","10");
Note: I'm new to stackoverflow.
Try this:
ObservableList<String> items = FXCollections.observableArrayList();
items.add("a");
items.add("b");
chobo_fisch.getItems().addAll(items);
Your combobox must be filled with items (in your case String):
List<String> list = new ArrayList<String>();
list.add("Item 1");
list.add("Item 2");
chobo_fisch.setItems(FXCollections.observableArrayList(list));
If you use a combobox of a more complex object, you could use a cellfactory to choose the value that is displayed :
chobo_fisch.setCellFactory(obj -> new ChoboFischListCell());
chobo_fisch.setButtonCell(new ChoboFischListCell());
where ChoboFischListCell is a class that extends ListCell and where you implement which field of your object should be displayed.
i want to design a GUI that contains BorderPane in the top of the container there is a MenuBar, in the left of the container there is a Acordion with different buttons that change the content of the center of the container for diferent FXML files, someting like the desktop app of Spotiffy
i got a working prototipe and it looks like this
the change in FXML happens and the buttos respont very well, the problem i have is that the FXML that fill the center part of the BoderPane doesn't auto rize and if is to big parts of the FXML doesn't show and if the FXML is smaller that the space left for the center part stays of the same small size and left alot of space with nothing
this is my code for the calling of the new FXML
public void lanzaUno(){
try {
// load first FXML
FXMLLoader loader = new FXMLLoader();
loader.setLocation(Coordinador.class.getResource(
"VistaControlador/Usuario/Uno.fxml"));
/*i put the AnchorPane inside of a
ScrollPane for the desire funcionality of alot
of vertical space for many nodes in a single FXML file
*/
ScrollPane unoAnchorPane = (ScrollPane) loader.load();
UnoController controller = loader.getController();
//example method for passing variables to the FXML controler
controller.pasoPrincipal(this, primaryStage, "Rhutt");
//puts the FXML in the center of the BorderPane
rootLayout.setCenter(unoAnchorPane);
//this if for trying to accommodate the content en the BorderPane
BorderPane.setAlignment(unoAnchorPane, Pos.TOP_LEFT);
} catch (IOException e) {
e.printStackTrace();
}
}
my first cuestion is what is the thing i am missing in the calling of the FXML for the contents of this ocupy the space available in the BorderPane?
my secon cuestion regards the change of FXML, when i pass fron one to another the change in the BorderPane is in a instant and looks very bad is there a way for making the transicion like one where the content of a FXML that is call push the content of the FXML in the center?, it doesn't have to be very elaborated just make the transition a little better
EDIT
i got a cordinator class where i send and recive parameters for all the FXML and where i declare the methods of calling new FXML, so i have a cordinator class, a FXML root with its controller and two FXML and its controllers with different things in each one, this two FXML are the ones that change in the BorderPane center of the root
this is the coordinator class
//Variables
private Stage primaryStage;
private BorderPane rootLayout;
/**
* launh
* #param primaryStage
*
*/
#Override
public void start(Stage primaryStage) throws Exception{
// Inicializa la escena
this.primaryStage = primaryStage;
this.primaryStage.setTitle("Login");
this.primaryStage.centerOnScreen();
//star method
iniLogin();
}
/**
*load the root scene
*/
public void iniLogin(){
try {
// Carga el loader.
FXMLLoader loader = new FXMLLoader();
loader.setLocation(com.aohys.rehabSys.MVC.Coordinador.class.getResource(
"VistaControlador/Pricipal/Principal.fxml"));
rootLayout = (BorderPane) loader.load();
//the root scene
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
// Da acceso al programa principal.
PrincipalController controller = loader.getController();
controller.pasoPrincipal(this, primaryStage);
primaryStage.centerOnScreen();
// Muesta la escena,
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
after this method there are two identical methods like the one at the beginning where i call the 2 changing FXML call LanzaUno, LanzaDos
this is my rood FXML controler
public class PrincipalController implements Initializable {
//variable of the coordinator class
private Coordinador cordi;
private Stage stage;
/**
* method for passing parameters to the FXML
* #param cordi
* #param stage
*/
public void pasoPrincipal(Coordinador cordi, Stage stage) {
this.cordi = cordi;
this.stage = stage;
}
//FXML in root
#FXML private Button btt1;
#FXML private Button btt2;
#FXML public static StackPane stackPane;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
//Call the firts FXML
btt1.setOnAction((evento)->{
cordi.lanzaUno();
});
//Call the second FXML
btt2.setOnAction((evento)->{
cordi.lanzaDos();
});
}
for the moment the controllers on the two FXML dont do anything
You should read up on Adam Bien's AfterburnerFX library for managing dependency injection and controllers in your application. It has changed my life. :)
For transitions, I use this code. It is a nice fade-out/fade-in from one screen to the next. In this case, stackPane is a StackPane in the center of your BorderPane defined in your FXML.
Here is a simple FXML with the aforementioned stackPane:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<BorderPane fx:id="borderPane" minHeight="-Infinity" minWidth="-Infinity" prefHeight="768.0" prefWidth="1280.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tada.gui.tada.TadaPresenter">
<center>
<StackPane fx:id="stackPane" minHeight="-Infinity" minWidth="-Infinity" prefHeight="240.0" prefWidth="320.0" BorderPane.alignment="CENTER" />
</center>
</BorderPane>
And the setScreen method that changes to the new one passed in:
/**
* Set Screen
*
* #param view
* #return boolean
*/
public boolean setScreen(Parent view) {
final DoubleProperty opacity = stackPane.opacityProperty();
if (!stackPane.getChildren().isEmpty()) { //if there is more than one screen
Timeline fade = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 1.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
stackPane.getChildren().remove(0); //remove the displayed screen
stackPane.getChildren().add(0, view); //add the screen
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
}, new KeyValue(opacity, 0.0)));
fade.play();
} else {
stackPane.setOpacity(0.0);
stackPane.getChildren().add(view); //no one else been displayed, then just show
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
return true;
}
You'll also need this in your controller...
private static final double TRANSITION_TIMER = 200;
EDIT:
I tried to put together a very basic "application". It is rudimentary, but I think it does a good job of illustrating the use of AfterburnerFX and the screen fade transition. There is a lot more to AfterburnerFX. I just used the view switching without the dependency injection, which is very important when you start wanting to work on objects in your application. Also, property binding is very important for a good UX. Anyway, Here is a link to my example on GitHub.
I am new to FXML and I am trying to create a handler for all of the button clicks using a switch. However, in order to do so, I need to get the elements using and id. I have tried the following but for some reason (maybe because I am doing it in the controller class and not on the main) I get a stack overflow exception.
public class ViewController {
public Button exitBtn;
public ViewController() throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("mainWindow.fxml"));
Scene scene = new Scene(root);
exitBtn = (Button) scene.lookup("#exitBtn");
}
}
So how will I get an element (for example a button) using it's id as a reference?
The fxml block for the button is:
<Button fx:id="exitBtn" contentDisplay="CENTER" mnemonicParsing="false"
onAction="#handleButtonClick" text="Exit" HBox.hgrow="NEVER" HBox.margin="$x1"/>
Use a controller class, so that you don't need to use a lookup. The FXMLLoader will inject the fields into the controller for you. The injection is guaranteed to happen before the initialize() method (if you have one) is called
public class ViewController {
#FXML
private Button exitBtn ;
#FXML
private Button openBtn ;
public void initialize() {
// initialization here, if needed...
}
#FXML
private void handleButtonClick(ActionEvent event) {
// I really don't recommend using a single handler like this,
// but it will work
if (event.getSource() == exitBtn) {
exitBtn.getScene().getWindow().hide();
} else if (event.getSource() == openBtn) {
// do open action...
}
// etc...
}
}
Specify the controller class in the root element of your FXML:
<!-- imports etc... -->
<SomePane xmlns="..." fx:controller="my.package.ViewController">
<!-- ... -->
<Button fx:id="exitBtn" contentDisplay="CENTER" mnemonicParsing="false" onAction="#handleButtonClick" text="Exit" HBox.hgrow="NEVER" HBox.margin="$x1" />
<Button fx:id="openBtn" contentDisplay="CENTER" mnemonicParsing="false" onAction="#handleButtonClick" text="Open" HBox.hgrow="NEVER" HBox.margin="$x1" />
</SomePane>
Finally, load the FXML from a class other than your controller class (maybe, but not necessarily, your Application class) with
Parent root = FXMLLoader.load(getClass().getResource("path/to/fxml"));
Scene scene = new Scene(root);
// etc...
I have a JavaFX application that has child windows or stages. The way I handle how all stages interact with each other is like this: I have a static collection of classes called GuiContainer in my Main class. The GuiContainer looks like this:
public class GuiContainer {
private final Stage stage;
private final Scene scene;
private final FXMLLoader loader;
private final Controller controller;
private final Parent parent;
public GuiContainer(Stage stage, Scene scene, FXMLLoader loader, Controller controller, Parent parent) {
this.stage = stage;
this.scene = scene;
this.loader = loader;
this.controller = controller;
this.parent = parent;
}
public Stage getStage() {
return stage;
}
public Scene getScene() {
return scene;
}
public FXMLLoader getLoader() {
return loader;
}
public Controller getController() {
return controller;
}
public Parent getParent() {
return parent;
}
public static final GuiContainer createMain(Stage stage, String title, int width, int height, String pathToView) throws Exception {
FXMLLoader loaderInner = new FXMLLoader(GuiContainer.class.getResource(pathToView));
Parent parentInner = loaderInner.load();
final Controller controllerInner = loaderInner.getController();
Scene sceneInner = new Scene(parentInner, width, height);
stage.setTitle(title);
stage.setScene(sceneInner);
GuiContainer guiContainer = new GuiContainer(
stage, sceneInner, loaderInner, controllerInner, parentInner
);
controllerInner.start(guiContainer);
return guiContainer;
}
public static final GuiContainer createModal(Window owner, String title, int width, int height, String pathToView) throws Exception {
if (owner == null) {
Log.error(GuiContainer.class.getSimpleName(), "Unable to create instance, missing window owner!");
return null;
}
Stage stageInner = new Stage();
FXMLLoader loaderInner = new FXMLLoader(GuiContainer.class.getResource(pathToView));
Parent parentInner = loaderInner.load();
Controller controllerInner = loaderInner.getController();
Scene sceneInner = new Scene(parentInner, width, height);
stageInner.setTitle(title);
stageInner.setScene(sceneInner);
stageInner.initStyle(StageStyle.DECORATED);
stageInner.initModality(Modality.WINDOW_MODAL);
stageInner.initOwner(owner);
GuiContainer guiContainer = new GuiContainer(
stageInner, sceneInner, loaderInner, controllerInner, parentInner
);
controllerInner.start(guiContainer);
return guiContainer;
}
}
This way I have access to any stage or controller from anywhere.
All possible GuiContainers are created only once at boot (in static main(String[] args)) and can then be statically accessed from anywhere, by anyone.
Now... in the main application (scene inside the primary stage) I have a TreeView that has a custom cell factory, and when a cell is right clicked an appropriate context menu is shown. Inside this context menu I open a child/modal stage, like so:
String containerName = "guiName";
GuiContainer container = Main.getGuiContainers().getOrDefault(containerName, null);
if(container == null) {
Log.error(getClass().getSimpleName(), "Unable to find GuiContainer: " + containerName);
return;
}
container.getStage().showAndWait();
Now, here comes the problem. JavaFX doesn't request focus on the child stage. For instance, I can't type into a TextField (on the child stage) because I have an onKeyPressed event registered on the primary stage and it captures all the keys I press. On this child stage I also have a ComboBox, and when I select an item from that ComboBox, the child stage finally comes into focus and the primary stage no longer captures the keys I press.
I also tried to change the modality to all posible values and read what they do on oracle.com, but none of the information helped me...
Is there something wrong with my code? Or could this be an issue with JavaFX? Any ideas?
EDIT: Here is my Application overriden start method: (I hope this provides more information)
#Override
public void start(Stage stage) throws Exception {
GuiContainer guiWindow1 = GuiContainer.createMain(stage, "Window1", 900, 460,
"/com/project/app/gui/views/Window1.fxml");
GuiContainer guiWindow2 = GuiContainer.createModal(stage, "Window2", 320, 240,
"/com/project/app/gui/views/Window2.fxml");
GuiContainer guiWindow3 = GuiContainer.createModal(stage, "Window3", 320, 240,
"/com/project/app/gui/views/Window3.fxml");
GuiContainer guiWindow4 = GuiContainer.createModal(stage, "Window4", 420, 360,
"/com/project/app/gui/views/Window4.fxml");
GuiContainer guiWindow5 = GuiContainer.createModal(stage, "Window5", 380, 460,
"/com/project/app/gui/views/Window5.fxml");
guiWindow5.getStage().setResizable(false);
guiContainers.put("guiWindow1", guiWindow1);
guiContainers.put("guiWindow2", guiWindow2);
guiContainers.put("guiWindow3", guiWindow3);
guiContainers.put("guiWindow4", guiWindow4);
guiContainers.put("guiWindow5", guiWindow5);
guiWindow1.getStage().show();
}
EDIT2: This is how I register the onKeyPressed listener: (in my parent controller init() method)
getGuiContainer().getScene().setOnKeyPressed((e) -> {
Log.info(getClass().getSimpleName(), "Key pressed: " + e.getCode().getName());
Macro macro = Main.getProject().getSelectedMacro();
if(macro == null) {
Log.error(getClass().getSimpleName(), "Unable to find selected macro");
return;
}
if (e.getCode() == KeyCode.ESCAPE) {
macro.getNodePlacement().reset();
macro.cancelSelect();
macro.repaint();
} else if(e.getCode() == KeyCode.DELETE) {
macro.deleteSelectedNodes();
}
});
EDIT3: This is how my abstract controller class looks like - I hope this provides a better insight
public abstract class Controller {
private GuiContainer guiContainer;
public final GuiContainer getGuiContainer() {
return guiContainer;
}
public final void start(GuiContainer guiContainer) {
this.guiContainer = guiContainer;
init();
}
public abstract void init(); // this is where the onKeyPressed is registered, when extending the abstract class
public abstract void reset();
public abstract void refresh();
}
IMPORTANT EDIT: This only happens on the MAC platform, I ran the same application on windows and there were no such problems.
could you please post the code of the constructor of your stage?
Just a note, from the doc:
Note that showing a modal stage does not necessarily block the caller.
The show() method returns immediately regardless of the modality of
the stage. Use the showAndWait() method if you need to block the
caller until the modal stage is hidden (closed). The modality must be
initialized before the stage is made visible.
EDIT - My mistake, I didn't notice the createModal() method.
What I ended up doing in some of my custom stages is:
Platform.runLater(() -> somecontrol.requestFocus());
(java 8, obviously, override run() method if lambda is not available with your current language level (< 8)).
But I only do this for non-modal stage. Modal stage should take the focus automatically.
According to the doc of Modality, APPLICATION_MODAL is supposed to do what you expect (including Blocking the events for other windows):
APPLICATION_MODAL Defines a modal window that blocks events from being
delivered to any other application window.
You call initModality with WINDOW_MODAL before calling initOwner. Please see:
WINDOW_MODAL public static final Modality WINDOW_MODAL Defines a modal
window that block events from being delivered to its entire owner
window hierarchy. Note: A Stage with modality set to WINDOW_MODAL, but
its owner is null, is treated as if its modality is set to NONE.
If requestFocus() (called after the modal stage has been displayed, and in the UI thread right?) doesn't take the focus, I guess your main window is automatically taking it back.