JavaFX 2 Window event handling in controllers - java

So I am trying to handle WINDOW_SHOWN event from my controller with code like this:
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
initializeDatePickers();
System.out.println("payer number in initialize: " + payerNumber);
URL location = getClass().getResource("/createUser.fxml");
FXMLLoader loader = new FXMLLoader();
try {
Parent root = (Parent) loader.load(location.openStream());
root.getScene().getWindow().setOnShown(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
System.out.println("ONSHOWN");
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
But all I've got was endless cycle and program crash.
The code below didn't work either, it returns NullPointerException:
#FXML private AnchorPane createUserDialog; //my root pane
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
createUserDialog.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_SHOWN,
new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent window) {
System.out.println("ONSHOWN");
}
});
}
Implementing WindowEvent interface didn't work at all, don't know why.
So, how could I handle this event? And why I've got NullPointerException? In docs said that initialize() calling only after root pane completely processed.

When the initialize() method is being executed, the root pane is completely constructed but is not added to a scene, or a window. (The initialize() method is executed as part of the execution of your FXMLLoader's load() method; check the code where you call that and you will see that you add the root to a scene and place it in a window after that.) So during the execution of intialize(), root.getScene() will return null.
You can use a Binding to check when the window changes and attach a listener to it:
final EventHandler<WindowEvent> shownHandler = new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
System.out.println("Shown");
}
};
Bindings.<Window>select(createUserDialog.sceneProperty(), "window").addListener(new ChangeListener<Window>() {
#Override
public void changed(ObservableValue<? extends Window> observable,
Window oldValue, Window newValue) {
if (oldValue != null) {
oldValue.removeEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
}
if (newValue != null) {
newValue.addEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
}
}
});
This code assumes the root is only ever added to one window; in the unlikely event you're taking the root out of one window and putting it in another during your application life cycle, you would need to remove the listener from the old window. If you need this I'll update the code, but it makes it more complex.

Related

How to register EventBus only once

I'm writing JavaFX application in order to send from one controller to other controller. I use EventBus which was written by developer. I download it from github.But When I try to recall from one controller to other controller. First time it works once. Second time it works twice. Third time it works three times and so on. What might be reason of behaving this eventbus?
MainController
here Event bus was registered like static
public class Main extends Application
{
public static EventBus eventBus = new FxEventBus();
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This controller class which was fired event
Controller
private AddClickedEvent addClickedEvent;
#Override
public void initialize(URL location, ResourceBundle resources)
{
id.setOnAction(event ->
{
try
{
FXMLLoader loader = new FXMLLoader();
Parent parent = loader.load(getClass().getResource("ask.fxml").openStream());
Stage stage = new Stage();
stage.setScene(new Scene(parent));
if(addClickedEvent == null){
addClickedEvent = new AddClickedEvent(AddClickedEvent.ANY);
}
Main.eventBus.fireEvent(addClickedEvent);
stage.showAndWait();
} catch (IOException e) {
e.printStackTrace();
}
});
}
Here is Controller other Controller that should show up something after fire
#Override
public void initialize(URL location, ResourceBundle resources)
{
Main.eventBus.addEventHandler(AddClickedEvent.ANY,event -> {
System.out.println("uyondan bosilib galdi");
System.out.println(yes);
yes = true;
});
id1.setOnAction(event -> {
System.out.println(yes);
});
id2.setOnAction(event -> {
Stage stage = (Stage)((Node) (event).getSource()).getScene().getWindow();
stage.close();
});
}
AddClicked Event class
public class AddClickedEvent extends Event
{
public static final EventType<AddClickedEvent> ANY =
new EventType<>(Event.ANY, "ADD_CLIENT_EVENT");
public AddClickedEvent(#NamedArg("eventType") EventType<? extends Event> eventType) {
super(eventType);
}
}
EventHandler should be created only once in process of whole application then it will react only once rather than multiple times. I come up with solutions to it declare for every controller class with static int variable which helps me to register events only once when the value of int is zero.
private static int check;
#Override
public void initialize(URL location, ResourceBundle resources)
{
if(check == 0)
{
Main.eventBus.addEventHandler(AddClickedEvent.ANY,event -> {
System.out.println("uyondan bosilib galdi");
System.out.println(yes);
yes = true;
});
check ++;
// Then it will react only once We registered event here
}
id1.setOnAction(event -> {
System.out.println(yes);
});
id2.setOnAction(event -> {
Stage stage = (Stage)((Node) (event).getSource()).getScene().getWindow();
stage.close();
});
}

Java FX - Progress bar update

I have a method that takes a while to complete, when the jar application is started. To have some feedback, i created the form frmWaiting, that displays a simple indeterminate progress bar. I also have a controller for the form,
PrincipalController.
Entry point for the application
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Stage stagePrincipal = new Stage();
Parent parentPrincipal = FXMLLoader.load(getClass().getClassLoader().getResource("frmPrincipal.fxml"));
Scene scenePrincipal = new Scene(parentPrincipal, 300, 275);
stagePrincipal.setScene(scenePrincipal);
stagePrincipal.setHeight(400);
stagePrincipal.setWidth(500);
stagePrincipal.setResizable(false);
stagePrincipal.setTitle("Instalador");
stagePrincipal.show();
} catch (Exception e) {
e.printStackTrace();
}
}
PrincipalController - frmPrincipal.fxml:
#Override
public void initialize(URL location, ResourceBundle resources) {
try {
Stage stageWaiting = new Stage();
Parent parentWaiting;
parentWaiting = FXMLLoader.load(getClass().getClassLoader().getResource("frmWaiting.fxml"));
Scene sceneWaiting = new Scene(parentWaiting, 300, 275);
stageWaiting.setScene(sceneWaiting);
stageWaiting.setHeight(300);
stageWaiting.setWidth(400);
stageWaiting.setResizable(false);
stageWaiting.setTitle("Instalador");
stageWaiting.show();
} catch (IOException e) {
}
}
WaitingController - frmWaiting.xml:
public class WaitingController implements Initializable {
#FXML private ImageView img;
#FXML private ProgressBar progressBar;
#FXML private ProgressIndicator pgIndicator;
private Task copyTask;
#Override
public void initialize(URL location, ResourceBundle resources) {
img.setImage(new Image(getClass().getClassLoader().getResourceAsStream("image.png")));
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
ArquivoController.getInstance().copiaArquivosPadrao(); //This is the method that takes a while.
}
public ProgressBar getProgressBar() {
return progressBar;
}
I want to initialize my main form, frmPrincipal, when my method that takes a while finishes. I also want to get the progress bar working. I have tried to do it on another Thread, but i could not get the response from it when the method finishes.
All the .fxml files are correct, ommited them to make things easier if possible.
The way it is, the application waits for the method to finish, then opens the other form. But, the progressBar does not update.
Inside ArquivoController.getInstance().copiaArquivosPadrao(); you have to update the progressBar's progress.
You could do it the nice way, using a Task to run copiaArquivosPadrao(), update the tasks progress accordingly and binding to the tasks progress property.
Or you could do it the ugly way, passing progressBar to copiaArquivosPadrao(), something like this:
public void initialize(URL location, ResourceBundle resources) {
img.setImage(new Image(getClass().getClassLoader().getResourceAsStream("image.png")));
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
ArquivoController.getInstance().copiaArquivosPadrao(progressBar); //This is the method that takes a while.
}
and
public void copiaArquivosPadrao(ProgressBar progressBar) {
// call progressBar.setProgress() in here to update the progress bar
// eg.
progressBar.setProgress(0.0F);
doSomething();
progressBar.setProgress(0.20F);
doSomething();
progressBar.setProgress(0.40F);
doSomething();
progressBar.setProgress(0.60F);
doSomething();
progressBar.setProgress(0.80F);
doSomething();
progressBar.setProgress(1.00F);
}
For sure, you can do this more fine-grained in a loop or similar.

Switching scene from controller by calling method in Application

I'm doing a project, where I have to switch scenes depending on user input. At the moment I have a Gui-Class, which is an Application and loads the first fxml-File, it is launched by a MainController-class:
public class MainController {
public MainController() {
Application.launch(Gui.class);
}
}
The Gui-Class:
public class Gui extends Application{
Stage primaryStage;
//I wanna ideally use this method to switch scenes
public void switchen(String nextScene) throws Exception
{
Parent nextS;
if(nextScene.equals("singleMenue"))
{
nextS = FXMLLoader.load(getClass().getResource("SingleMenue.fxml"));
primaryStage.hide();
primaryStage.setScene(new Scene(nextS, 900, 600));
primaryStage.show()
}
}
#Override
public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
Parent decisionScene = FXMLLoader.load(getClass().getResource("Decision.fxml"));
primaryStage.setTitle("title");
primaryStage.setScene(new Scene(decisionScene, 900, 600));
primaryStage.show();
}
I also have a different Controller-class for each .fxml-file (this is the first):
public class DecisionController {
#FXML
private Button singleButton;
#FXML
private Button multiButton;
public DecisionController() {
}
#FXML
private void buttonPressed(ActionEvent event) throws IOException
{
if(event.getSource() == singleButton)
{
System.out.println("single");
// I wanna somehow call switch("singleMenue"); here
}
else if(event.getSource() == multiButton)
{
System.out.println("multi");
}
}
}
So how do I switch the Scenes from inside the Controller now ?
I tried to generally go with the MVC-pattern but I don't know how it should be possible for my DecisionController-Object to call a method inside the Gui-Application. I would need to reach inside a running Thread here, right ?
I also don't know how I could turn over my Gui-Instance to my DecisionController, since the latter one is initialized from the Decision.fxml.
One other thing I tried was to switch the Scene directly from the DecisionController with:
#FXML
private void buttonPressed(ActionEvent event) throws IOException
{
if(event.getSource() == singleButton)
{
System.out.println("single");
Stage mainstage = (Stage) singleButton.getScene().getWindow();
Parent singleScene = FXMLLoader.load(getClass().getResource("SingleMenue.fxml"));
mainstage.setScene(new Scene(singleScene, 900, 600));
}
else if(event.getSource() == multiButton)
{
System.out.println("multi");
}
}
This understandably doesn't work either since manipulation from outside the application thread isn't possible.
I'm kind of new to JavaFX & FXML so my general idea for the structure of the program might just be totally wrong, maybe this isn't how you MVC. Any feedback is much appreciated :)

JavaFX+Afterburner.fx access parent window from child

I have a program with this window structure:
Where left side has buttons and green zone is different windows that change where user clic on a left button.
I have a main class (call Estructura) thats acts as control for main window except green panel. For green panel, I inject (thanks to afterburner.fx DI) correspond panel base on user button selection from left side. Injected panel has its own controller that is lazy thanks to DI Framework base on MVP.
public class EstructuraPresenter implements Initializable {
private static final Logger LOG = getLogger(EstructuraPresenter.class.getName());
#FXML
ToggleButton btnPlantillas, btnAlumnos, btnEstadisticas;
#FXML
BorderPane pEstructura; //Correspond to all window except green zone
#FXML
StackPane pContenedor; //Green zone
//Injection of all child windows that i want to show in green zone
#Inject
private AlumnosView alumnosview;
#Inject
private PlantillasView plantillasview;
#Inject
private EstadisticasView estadisticasview;
#Override
public void initialize(URL url, ResourceBundle rb) {
//I set up green zone with alumnos panel so I call method whit request section to load
cambiarSeccion("Alumnos");
btnPlantillas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Plantillas");
});
btnAlumnos.setOnAction((ActionEvent event) -> {
cambiarSeccion("Alumnos");
});
btnEstadisticas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Estadisticas");
});
}
//I pass to this method, name of window/panel that I like to load in green zone
public void cambiarSeccion(String nombreVentana) {
try {
//First, reset all buttons (so when user select an option and enter in case, I select option button making like effect as a selected)
btnAlumnos.setSelected(false);
btnPlantillas.setSelected(false);
btnEstadisticas.setSelected(false);
switch (nombreVentana) {
case "Alumnos":
if (btnAlumnos.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Alumnos");
btnAlumnos.setSelected(true);
//I need to do this check because for first time program load because green zone hasn't got any previous panel load
if (pContenedor.getChildren().contains(alumnosview.getView())) {
pContenedor.getChildren().clear();
alumnosview.getViewAsync(pContenedor.getChildren()::add);
} else {
pContenedor.getChildren().add(alumnosview.getView());
}
}
break;
case "Plantillas":
if (btnPlantillas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Plantillas");
btnPlantillas.setSelected(true);
pContenedor.getChildren().clear();
plantillasview.getViewAsync(pContenedor.getChildren()::add);
}
break;
case "Estadisticas":
if (btnEstadisticas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Estadísticas");
btnEstadisticas.setSelected(true);
pContenedor.getChildren().clear();
estadisticasview.getViewAsync(pContenedor.getChildren()::add);
}
break;
//There are more cases but I use same configuration like above examples...
}
}
catch (Exception e) {
LOG.log(Level.SEVERE, e.toString());
new Dialogos().mostrarExcepcion(null, e);
}
}
}
Well, when user clic on left button, panel is load ok in green zone. But problem I have is where user for example in green zone clic on a button/link... to load another panel (similiar to above description) but in that case I need to call from green panel controller to method cambiarSeccion() that is in EstructuraPresenter controller.
So for example, on case of user select estadisticas button (i hide in image, sorry I tried to simplified), that's is estadisticaspresenter controller:
public class EstadisticasPresenter implements Initializable {
private static final Logger LOG = getLogger(EstadisticasPresenter.class.getName());
#FXML
Hyperlink linkTotalAlumnos;
#Inject
private EstructuraView estructuraview;
#Override
public void initialize(URL url, ResourceBundle rb) {
linkTotalAlumnos.setOnAction((ActionEvent event) -> {
//This call apparently works, I debug and call to method cambiarSeccion happend but screen isn't update
((EstructuraPresenter) estructuraview.getPresenter()).cambiarSeccion("Alumnos");
});
}
}
Where estadisticas window is loaded If I click on left buttons, works but If I clic on green load panel nothing happend. I resume problem in next image:
In my opinion your approach has one problem: When you call in any of your children presenters
((EstructuraPresenter) estructuraview.getPresenter())
.cambiarSeccion("Alumnos");
estructuraview.getPresenter() is actually creating a new instance of the main presenter.
This means that EstructuraPresenter.initialize() is being called again when you click on the hyperlink, and you have two calls to cambiarSeccion() (one from Initialize, one from the action), that add the children to the new instance, not to the old one, which is the one you see. That's why you don't see any change!
I'd suggest a different approach: Let the main presenter listen to changes in some properties on their childrens.
For example, add a boolean property to notify a click on the hyperlink:
public class EstadisticasPresenter implements Initializable {
#FXML Hyperlink linkTotalAlumnos;
private final BooleanProperty link = new SimpleBooleanProperty();
public boolean isLink() { return link.get(); }
public void setLink(boolean value) { link.set(value); }
public BooleanProperty linkProperty() { return link; }
#Override public void initialize(URL url, ResourceBundle rb) {
linkTotalAlumnos.setOnAction((ActionEvent event) -> {
link.set(true);
});
}
}
while on the main presenter:
#Override
public void initialize(URL url, ResourceBundle rb) {
...
EstadisticasPresenter estadisticas =
(EstadisticasPresenter)estadisticasview.getPresenter();
estadisticas.linkProperty().addListener((ob,b,b1)->{
if(b1){
cambiarSeccion("Alumnos");
// reset link property
estadisticas.setLink(false);
}
});
}
As a side note, instead of clear() and add, I'd use pContenedor.getChildren().setAll(<view>); since you only show one child at a time.
Base on #josé-pereda answer, finally I can link button/hyperlink from sub-window (green area) to main controller. The advantage of my solution is that I avoid use of boolean variables.
In EstructuraPresenter controller I add listeners to buttons from EstadisticasPresenter(which is the controller from one of the sub-window):
public class EstructuraPresenter implements Initializable {
private static final Logger LOG = getLogger(EstructuraPresenter.class.getName());
#FXML
ToggleButton btnAlumnos, btnEstadisticas;
#FXML
StackPane pContenedor;
#Inject
private AlumnosView alumnosview;
#Inject
private EstadisticasView estadisticasview;
#Inject
private DataModel datos;
#Override
public void initialize(URL url, ResourceBundle rb) {
((EstadisticasPresenter) estadisticasview.getPresenter()).getLinkTotalAlumnos().setOnAction((ActionEvent event) -> {
this.cambiarSeccion("Alumnos");
});
//This call is from first time, where program starts to show by default Alumnos sub-window
cambiarSeccion("Alumnos");
btnAlumnos.setOnAction((ActionEvent event) -> {
cambiarSeccion("Alumnos");
});
btnEstadisticas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Estadisticas");
});
}
public void cambiarSeccion(String nombreVentana) {
try {
btnAlumnos.setSelected(false);
btnEstadisticas.setSelected(false);
switch (nombreVentana) {
case "Alumnos":
if (btnAlumnos.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Alumnos");
btnAlumnos.setSelected(true);
alumnosview.getViewAsync(pContenedor.getChildren()::setAll);
pContenedor.getChildren().setAll(alumnosview.getView());
}
break;
case "Estadisticas":
if (btnEstadisticas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Estadísticas");
btnEstadisticas.setSelected(true);
estadisticasview.getViewAsync(pContenedor.getChildren()::setAll);
}
break;
}
}
catch (Exception e) {
LOG.log(Level.SEVERE, e.toString());
new Dialogos().mostrarExcepcion(null, e);
}
}
}
NOTE In addition, I change getChildren().clear() && getChildren().add() to getChildren().setAll(); These prevents re-initializate controllers from each window that I put in green area.
estadisticasview.getViewAsync(pContenedor.getChildren()::setAll);
And in EstadisticasPresenter controller, I add getters from that controls that I want to user clic to go to another window (so when user clic on control, green area change with another window/pane).
public class EstadisticasPresenter implements Initializable {
private static final Logger LOG = getLogger(EstadisticasPresenter.class.getName());
#FXML
Hyperlink linkTotalAlumnos;
#Inject
private DataModel datamodel;
#Override
public void initialize(URL url, ResourceBundle rb) {
}
//Getters that I use in EstructuraPresenter controller to set up event handlers
public Hyperlink getLinkTotalAlumnos() {
return linkTotalAlumnos;
}
}

How to focus a specific node when selected tab changes in JavaFX?

I want to set focus to a specific node in the content of a Tab. I added a ChangeListener to selectedItem property as follows (assume that the class contains a field named secondNode of type Node):
tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
#Override
public void changed(ObservableValue<? extends Tab> observable, Tab oldValue, Tab newValue) {
if (newValue != null) {
Platform.runLater(new Runnable() {
#Override
public void run() {
secondNode.requestFocus();
}
});
}
}
});
However, this does not work. I assume the reason is because TabPane performs some additional actions, which affect focus, after the new tab has been selected (but, looking through TabPane source code, I can't figure out what). If I single-step through this code in a debugger, it works as expected, so it appears to be a race condition. If this is so, how can this be resolved?
You could try replacing the Runnable with a Task:
new Thread( new Task<Void>()
{
#Override
public Void call() throws Exception // This is NOT on FX thread
{
Thread.sleep(100);
return null;
}
#Override
public void succeeded() // This is called on FX thread.
{
secondNode.requestFocus();
}
}).start();

Categories