JavaFx TableView - update items from another controller - javafx

I have a TableView that I am populating with 2 columns (Key and Value) from a database. When I click on a table row, I open a new scene which has a text area that shows the Value column and allows one to edit it. I choose to go with a separate UI for editing the contents of value column as it contains a prettyfied JSON document and having an in place edit in the table would have been cumbersome.
dataTable.setRowFactory(tv -> {
TableRow<Map.Entry<String, String>> row = new TableRow<>();
row.setOnMouseClicked(event -> {
showDataPopup(dataValue.getKey(), dataValue.getValue());
});
return row;
});
private void showDataPopup(String key, String value) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("tableDataPopup.fxml"));
Parent dataRoot = (Parent) fxmlLoader.load();
Stage stage = new Stage();
stage.setTitle("Data Viewer");
stage.setScene(new Scene(dataRoot, 800, 500));
DataPopupController dataPopupController = fxmlLoader.getController();
dataPopupController.loadDataTextArea(key, value, this);
stage.show();
} catch (Exception e) {
logger.error("Error loading tableDataPopup.fxml", e);
}
}
Now, in the tableDataPopup scene, I allow the value to be edited. I have a save button which should save the edited document back to the table and close the scene. Here's my save method
public void saveEditedDocument(ActionEvent event) {
//code to save document to db
mainController.refreshTable(docIdLabel.getText(), dataTextArea.getText());
Stage stage = (Stage) editCancelButton.getScene().getWindow();
stage.close();
}
...
I have a refreshTable method in my main controller. Main controller has all the TableView components and logic.
public void refreshTable(String docId, String docVal) {
logger.info(": {}", dataTable.getItems());
}
I need help figuring out how to update the cell value that was changed in the popup dialog. I'd rather avoid having to stream the whole table and look for the key column and update the value. I am looking for a way to pass the cell index to the data popup and have it pass it back to the refreshTable method. Then use it to directly update the cell and then call dataTable.refresh() method to refresh the data table.
I am struggling with where to even start on this. Any pointers would really help...

I guess i figured it out. May not be an elegant solution, but this works for me.
I am passing the index of the row that was clicked on to my dataPopupController
dataTable.setRowFactory(tv -> {
TableRow<Map.Entry<String, String>> row = new TableRow<>();
row.setOnMouseClicked(event -> {
showDataPopup(dataValue.getKey(), dataValue.getValue(),row.getIndex());
});
return row;
});
private void showDataPopup(String key, String value, int index) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("tableDataPopup.fxml"));
Parent dataRoot = (Parent) fxmlLoader.load();
Stage stage = new Stage();
stage.setTitle("Data Viewer");
stage.setScene(new Scene(dataRoot, 800, 500));
DataPopupController dataPopupController = fxmlLoader.getController();
dataPopupController.loadDataTextArea(key, value, this, **index**);
stage.show();
} catch (Exception e) {
logger.error("Error loading tableDataPopup.fxml", e);
}
}
And I updated the refreshTable method in main controller to update the item based on the index
public void refreshTable(String docVal, int tableIndex) {
logger.info(": {}", dataTable.getItems());
logger.info("Table value is: {}", dataTable.getItems().get(tableIndex).setValue(docVal));
dataTable.refresh();
}
And in my popup controller, I just pass back the index when I call refreshTable method
public void saveEditedDocument(ActionEvent event) {
// Todo - refresh table after save
try {
mainController.refreshTable(minifyData(dataTextArea.getText()), tableIndex);
Stage stage = (Stage) editCancelButton.getScene().getWindow();
stage.close();
} catch (JsonSyntaxException exception) {
saveStatusLabel.setText("Malformed JSON, please correct");
logger.error("Malformed JSON - {}", exception.getMessage());
}
}
May not be the most elegant way to do this, but in a pinch, it works for me.

Related

Context menu on TableRow<Object> does not show up on first right click

So I followed this example on using context menu with TableViews from here. I noticed that using this code
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
does not show up on first right click on a row with values. I need to right click on that row again for the context menu to show up. I also tried this code(which is my first approach, but not using it anymore because I've read somewhere that that guide is the best/good practice for anything related about context menu and tableview), and it displays the context menu immediately
if (row.getItem() != null) {
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}
but my problem with this code is it throws a NullPointerException whenever i try to right click on a row that has no data.
What could I possibly do to prevent NullPointerException while having the context menu show up immediately after a right click? In my code, I also have a code that a certain menu item in the context menu will be disabled based on the property of the myObject binded to row, that's why i need the context menu to pop up right away.
I noticed this too with the first block of code. Even if the property of myObject has already changed, it still has a menu item enabled/disabled unless I right click on that row again. I hope that you could help me. Thank you!
Here is a MCVE:
public class MCVE_TableView extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane myBorderPane = new BorderPane();
TableView<People> myTable = new TableView<>();
TableColumn<People, String> nameColumn = new TableColumn<>();
TableColumn<People, Integer> ageColumn = new TableColumn<>();
ContextMenu rowMenu = new ContextMenu();
ObservableList<People> peopleList = FXCollections.observableArrayList();
peopleList.add(new People("John Doe", 23));
nameColumn.setMinWidth(100);
nameColumn.setCellValueFactory(
new PropertyValueFactory<>("Name"));
ageColumn.setMinWidth(100);
ageColumn.setCellValueFactory(
new PropertyValueFactory<>("Age"));
myTable.setItems(peopleList);
myTable.getColumns().addAll(nameColumn, ageColumn);
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>();
row.setOnContextMenuRequested((event) -> {
People selectedRow = row.getItem();
rowMenu.getItems().clear();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
if (selectedRow != null) {
if (selectedRow.getAge() > 100) {
sampleMenuItem.setDisable(true);
}
rowMenu.getItems().add(sampleMenuItem);
}
else {
event.consume();
}
/*if (row.getItem() != null) { // this block comment displays the context menu instantly
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}*/
// this requires the row to be right clicked 2 times before displaying the context menu
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
});
return row;
});
myBorderPane.setCenter(myTable);
Scene scene = new Scene(myBorderPane, 500, 500);
primaryStage.setTitle("MCVE");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main (String[] args) {
launch(args);
}
}
Here is the People Class
public class People {
SimpleStringProperty name;
SimpleIntegerProperty age;
public People(String name, int age) {
this.name = new SimpleStringProperty(name);
this.age = new SimpleIntegerProperty(age);
}
public SimpleStringProperty NameProperty() {
return this.name;
}
public SimpleIntegerProperty AgeProperty() {
return this.age;
}
public String getName() {
return this.name.get();
}
public int getAge() {
return this.age.get();
}
}
Edit: MCVE added
Edit2: Updated the MCVE. Still requires to be right-clicked twice before the contextMenu pops up
Below's a code snippet as a quick demonstration of how-to/where-to instantiate and configure a per-row ContextMenu. It
creates a ContextMenu/MenuItem for each TableRow at the row's instantiation time
creates a conditional binding that binds the menu to the row's contextMenuProperty if not empty (just the same as you did)
configures the contextMenu in an onShowing handler, depending on the current item (note: no need for a guard against null, because the conditional binding will implicitly guarantee to not show the the menu in that case)
The snippet:
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>() {
ContextMenu rowMenu = new ContextMenu();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
{
rowMenu.getItems().addAll(sampleMenuItem);
contextMenuProperty()
.bind(Bindings
.when(Bindings.isNotNull(itemProperty()))
.then(rowMenu).otherwise((ContextMenu) null));
rowMenu.setOnShowing(e -> {
People selectedRow = getItem();
sampleMenuItem.setDisable(selectedRow.getAge() > 100);
});
}
};
return row;
});

How to ensure each Controller can see the model and is only initialized once?

In my mainController I'm using the following method to change views:
#FXML
public void handleChangeView(ActionEvent event) {
try {
String changeButtonID = ((Button) event.getSource()).getId();
FXMLLoader loader = new FXMLLoader(getClass().getResource("../../resources/view/" + changeButtonID + ".fxml"));
mainView.setCenter(loader.load());
} catch (IOException e){
e.printStackTrace();
}
}
Now this is working but I have two problems:
1) With each button click the controller of the loaded FXML file fires again. For example in my other controller I have:
Timeline counterTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> System.out.println("Test")));
counterTimeline.setCycleCount(Animation.INDEFINITE);
counterTimeline.play();
With each click it seems a new Timeline is created and set to play - how can I ensure this doesn't happen? It seems like every time a change view button is clicked the controller for it is re-initialized.
2) How can I ensure each controller can see the model while still using the above method to change views? I'd rather avoid dependancy injections because I honestly can't wrap my head around it - after 6 hours trying to get afterburner.fx to work I can't handle it.
You can create a new fxml nodes only once per type:
private final Map <String, Parent> fxmls = new HashMap<>();
#FXML
public void handleChangeView(ActionEvent event) {
try {
String changeButtonID = ((Button) event.getSource()).getId();
Parent newOne = fxmls.get(changeButtonID);
if (newOne == null) {
FXMLLoader loader = new FXMLLoader(getClass().getResource("../../resources/view/" + changeButtonID + ".fxml"));
newOne = loader.load();
fxmls.put(changeButtonID, newOne):
}
mainView.setCenter(newOne);
} catch (IOException e){
e.printStackTrace();
}
}

Call a method of a controller from another controller JavaFX

I have a problem, i'm doing a program that simulates the FCFS algorithm. I'm working with threads. The thing is that when I press T the program needs to interrupt the thread and show a window(stage) with a table, when I close the stage i need to start again the thread, so my idea is that from the second controller call the function of the other controller and then close the stage
Here is where i call the second stage:
case T:
banderaPause=true;
th.interrupt();
Stage stTableView = new Stage();
FXMLLoader loader = new FXMLLoader();
Pane root = null;
try {
root = loader.load(getClass().getResource("showTable.fxml").openStream());
showTableController stController = (showTableController)loader.getController();
stController.init(arreglo);
stTableView.setTitle("Programa 4");
stTableView.setScene(new Scene(root, 950, 375));
stTableView.setX(2.00);
stTableView.setY(300.00);
stTableView.show();
stTableView.setOnCloseRequest(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
System.out.println("ya cerre");
}
});
} catch (IOException e1) {
e1.printStackTrace();
}
break;
And here is where i tried to call the method from the second controller:
case C:
try {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("second.fxml"));
Parent root = loader.load();
Controller n = loader.getController();
n.Continua();
Stage stage = (Stage) closeButton.getScene().getWindow();
stage.close();
} catch (IOException e1) {
e1.printStackTrace();
}
break;
But the stage just close and the method was never called
From the javadoc for onCloseRequested (emphasis mine):
Called when there is an external request to close this Window.
So the handler is not executed, if you call close yourself.
For Stages that are not the primary stage there is an option to wait for the window to be closed, however: Stage.showAndWait, so you could use this instead of registering an event handler:
stTableView.showAndWait();
// Print to console after stage is closed
System.out.println("ya cerre");

JavaFX secondary stage onCloseRequest

I have a little issue close a secondary stage after clicking the close at the top right corner.
I'm using fxml with controller class, i need a way to handle this situation.
Here is what i do but i get a nullpointer exception :
#Override
public void initialize(URL location, ResourceBundle resources) {
Stage stage = (Stage) tbTabPaneHome.getScene().getWindow();
stage.setOnCloseRequest(e -> {
Platform.exit();
System.exit(0);
});
}
Because the stage not yet intialized completly, so any other ideas ?
Since the Scene nor the Stage are created yet, you can't call them or you get a NPE, as you already mentioned.
One way to install the event handler on the stage will be listening to changes in the sceneProperty() of tbTabPaneHome.
Once the node is added to the scene, that property will give you the Scene instance.
But the scene is not added to the Stage yet, so you need to wait till this is done, with Platform.runLater():
public void initialize() {
tbTabPaneHome.sceneProperty().addListener((obs, oldScene, newScene) -> {
Platform.runLater(() -> {
Stage stage = (Stage) newScene.getWindow();
stage.setOnCloseRequest(e -> {
Platform.exit();
System.exit(0);
});
});
});
}
Did you try to deal with your secondary stage entirely in the main stage controller?
I want to hide or show a help windows from a button or help menu in my main application controller. Something like the following:
public Button helpBtn;
Stage anotherStage = new Stage();
boolean secondaryInitialyzed = false;
boolean secondaryShowing = false;
public void showOrHideHelp(ActionEvent actionEvent) throws IOException {
if (!secondaryInitialyzed){
Parent anotherRoot = FXMLLoader.load(getClass().getResource("mySecondaryStage.fxml"));
anotherStage.setTitle("Secondary stage");
Scene anotherScene = new Scene(anotherRoot, 500, 350);
anotherStage.setScene(anotherScene);
secondaryInitialyzed = true;
}
if (secondaryShowing){
anotherStage.hide();
secondaryShowing = false;
helpBtn.setText("Show Help");
}
else {
anotherStage.show();
secondaryShowing = true;
helpBtn.setText("Hide Help");
}
It does work, and there might be a way for you to handle your setOnCloseRequest within the main controller.
I have the opposing issue, i.e preventing closing the secondary stage window by clicking the close at the top right corner. I'll look into setOnCloseRequest and see if there is a way there.
I also have an other unrelated problem: can I position the secondary in reference to the primary one?

[A]How to use setFullScreen methode in javafx?

I have a stage with these properties
stage = new Stage(StageStyle.DECORATED);
stage.setFullScreen(true);
stage.setFullScreenExitHint("");
stage.setTitle("My App");
stage.getIcons().add(new Image(icon-address));
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("start.fxml"));
fxmlLoader.setController(this);
...
...//and other properties
It is in full screen in this stage. I have a button and each time I click on it , a method of another class is called and the reference of this stage is passed to it.
For example, if I have two classes Start and ButtonClicked
In Start, I have this :
Button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
buttonClicked a = new buttonClicked ();
try {
a.render(stage);
} catch (Exception ex) {
Logger.getLogger(menuController.class.getName())
.log(Level.SEVERE, null , ex);
}
}
});
Now, the render method is called and I have the following code inside render :
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("buttonclicked.fxml"));
fxmlLoader.setController(this);//set Controller for our fxml file
loader = fxmlLoader.load();//loader is a stackPane because buttonclick.fxml has stackPane
stage.setScene(new Scene(loader));
stage.setFullScreen(true);
...
The problem is, if user clicks on button, stage become a 1024x700(because i have pictures in start.fxml and buttonclicked.fxml. The size of both the picture is 1024x700
After a moment, it becomes full screen. How can i fix this ?
I want to kill the delay in full screen
Thanks
Try
stage.getScene().setRoot(loader);
in render method.
By doing this, the creation of a new scene is eliminated. Therefore I guess, some UI related updates and calculations that must be done after the scene has been shown, are also eliminated.

Resources