Can a JavaFX node's parent class by accessed/determined? - java

Add Note: While I didn't find a clean solution to my stated problem, the root issue turned out to be that I was attempting to solve the "wrong problem". This is due to the timing of when initialize() is being called on the different JavaFX objects, and got ugly quickly (i.e., what happens if/when GridPane accesses Tab before the appropriate value(s) are set on Tab?). The real solution was to step back, reassess the implementation, and use setUserData() on GridPane from Tab, after Tab's values were correctly populated. Still a bit of a kludge, but much cleaner and reliable than what I was originally attempting, which was requiring the solution asked for below.
I am adding a GridPane to a Tab, and I need to access Tab.getText(). In the GridPane's initialize(), I can get the Parent using GridPane.getParent(). But, Parent doesn't implement getText(), and I cannot do ( Tab )Parent, nor use instanceof.
I've found mechanisms for gaining access to GridPane's controller, but I really don't want to do that unless necessary (i.e., I'd like for the GridPane instance to do "the right thing" without having external prodding).
I know the code snippet below doesn't compile/run, but is there a clean way to accomplish the idea behind the code?
#FXML private GridPane gridPane;
#FXML
public void initialize() {
Parent parent = gridPane.getParent();
if (parent instanceof Tab) {
String foo = (( Tab )parent).getText();
}
}

There's no particularly clean way to do this. Once the tab has been placed in a scene, etc, you can iterate up the node hierarchy until you find a tab pane, then iterate through the tab pane's list of tabs and see if the content matches the node. E.g.:
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
public class TabPaneTest extends Application {
#Override
public void start(Stage primaryStage) {
TabPane pane = new TabPane();
Tab tab = new Tab("Test tab");
Label label = new Label("Test Label");
tab.setContent(label);
pane.getTabs().addAll(tab);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
for (Node n = label ; n != null ; n = n.getParent()) {
if (n instanceof TabPane) {
System.out.println("Found Tab Pane...");
for (Tab t : ((TabPane)n).getTabs()) {
if (t.getContent() == label) {
System.out.println("Tab containing label: "+t.getText());
}
}
}
}
}
public static void main(String[] args) {
launch(args);
}
}
You probably want to find a better way to do this though. E.g. can you just inject the tab directly instead of (or as well as) injecting the grid pane. It seems that whatever you need to do should be handled at a level which "knows about" the tab anyway. At worst, you could set the user data of the grid pane to the text of the tab. But this feels like an x-y problem: you shouldn't need to do this at all.

Easiest way I've found to do this is just to create a subclass of Gridpane and feed it the Tab it's being added to when you construct it.

It's been a while since I asked this, and I have since modified the implementation and this is no longer an issue.
The solution was to forgo declaring the controller in the FXML, and associating the controller to the FXMLLoader instance programmatically. This allows information to be passed to the GridPane's controller (either via constructor or other public methods) prior to the GridPane being loaded/instantiated.
Doing it this way, the required information is already resident in the controller and can be accessed during initialization.
Live and learn...

Related

Javafx Controlling UI from separate class, can't add node from calling class

I am experimenting with Javafx for a simple chess-like game.
From my start class, I am successful in using another class, "UIBuilder", to modify "Stage primaryStage". My UIBuilder method creates a Scene, adds a GridPane, and fills the GridPane with nodes (ImageView pictures, but this should be arbritary).
This is my code for my "UIBuilder" constructor, this 100% works at creating the stage, showing the nodes, etc.
I am trying to add a method to the UIBuilder class, that I can launch from my start class. All I want to do is change a specific node of the static GridPane rootGrid. E.g if rootGrid is an 8x8 grid of images, e.g chess, and I want to the node in 7,7 to a button in lieu of an image. I know that using static nodes is perhaps not best practice, but I am trying to get a hang of the basic gui creation before worrying 100% about the little details. Considering the nodes are static, I'm especially confused. Any general advice about JavaFX usage across classes would be helpful. I've checked through 8 pages of StackOverflow and haven't encountered this issue. Thanks for your patience.
I have tried returning the static GridPane rootGrid to the main class, and then using
GridPane root = UIBuilder.getGrid()
root.add(arbitraryButton, 7, 7);
I have also tried simply using a method within the UIBuilder class to simply create a button and add it to the static gridPane rootGrid. For this I tried passing the Scene and GridPane using getters from the UIBuilder, thinking that maybe I could pass them back into the UIBuilder to then add a button.
public static void buttonTest(Scene testScene, GridPane passGrid) {
Button testButton = new Button("Test");
passGrid.add(testButton, 8, 8);
}
Class variables
public static Stage cosmos;
public static GridPane rootGrid;
public static Scene rootScene;
public static String testString;
public static Button returnButton;
UIBuilder(Stage mainStage) {
GridPane rootGrid = new GridPane();
Scene scene = new Scene(rootGrid,1000,1600);
rootScene = scene;
mainStage.setScene(scene);
mainStage.show();
stageTestSize(mainStage, rootGrid);
rootGrid.setVgap(15);
rootGrid.setHgap(15);
randomUnits(rootGrid);
}
Under my start method, the constructor below references above code and works perfectly.
UIBuilder uiMaster = new UIBuilder(primaryStage);
Scene exampleScene = UIBuilder.getScene();
GridPane exampleGrid = uiMaster.getGrid();
Your rootGrid is now your main panel. Gridpanes take Node objects. You can do
Label myNode=new Label();
myNode.setGraphic(someOtherNodeOrPictureOrSomething);
int row=0;
int column=0;
rootGrid.add(myNode, column, row);
It's best to set all this up before you .show() because once it is shown, you must use Platform.runlater(()->{...}); to make changes to components. Before the stage is shown, you can create and alter the nodes without lagging the UI.

Force relayout of Java FX Chart programatically

I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater() does not work.
I know that my changes are present in the chart because
(1) When I resize the chart by hand my changes suddenly appear
(2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout() while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout() inside of a runLater() would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
#Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
#Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships:
Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
#Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
You don't need to cast that node into a StackPane and set sizes. You need to use the setScaleX() and setScaleY() Methods
Node node = series.getData().get(0).getNode();
node.setScaleX(20);
node.setScaleY(20);
#Override
public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
Node node = data.getNode();
node.setScaleY(20);
node.setScaleX(20);
}
}
}
#c0der posted a solution in comment form to my original post which worked but produced a runtime warning (in Eclipse). His solution was to add a dummy style sheet at the lineChart level with lineChart.getStylesheets().add(""); after the for loops ended. This code produced the warning "Apr 28, 2020 9:01:12 AM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."
What did work without causing a run-time warning was to load an empty .css file and add it as a StyleSheet:
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm());
// note: I keep my .css resource files at the same location as my CSS class
// which is why I have the code "CSS.class" above
This one line solution worked once but I doubted it would work multiple times. I tested it to by increasing the size of the StackPane by 5 for each successive time "Resize ALL the points" was selected (in my dummy code above). Sure enough, it only worked the first time.
However, I added the no-op code lineChart.getStylesheets().replaceAll((s)->s+" "); before that one line solution and now it works multiple times in a row.
No matter how many times I executed the two lines of code
lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); `
it (1) worked and (2) The size of the list of lineChart StyleSheets did not grow beyond a size of 1. So a solution with a mystery.
Note: if you have an existing style sheet (I did not in my dummy example above) lineChart.getStylesheets().replaceAll((s)->s+" "); by itself may work as well. For some reason lineChart.getStylesheets().replaceAll((s)->s); without adding the " " on the end did not work.
Note: I originally thought I would have to code up a toggle solution to add Empty.css and the remove Empty.css with alternate calls but that proved unnecessary.
Bottom Line: if you have an existing StyleSheet lineChart.getStylesheets().replaceAll((s)->s+" "); works. If you do not have an existing StyleSheet adding an empty .css file as a StyleSheet in conjunction with the above replaceAll works.
Thanks again to #c0der for his original approach.

JavaFX ComboBox becomes unclickable after removal and re-adding

I think I perhaps have found a bug in Java, or maybe I am doing something wrong.
I populate a container based on some received data. The container has one or more ComboBoxes. On ComboBox selection change I receive new data. I then clear the GridPane and re-add the nodes (that still exist in the new data, and/or add new nodes).
The ComboBox still has focus, but I am unable to activate it again on click. Anything which causes the ComboBox to lose focus (such as focusing another component) will cause it to work again.
This is an simplified example. Tried with jdk1.8.0_162 and jdk-9.0.4
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application {
public static void main(String[] args) {
ComboBoxTest.launch(args);
}
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
root.getChildren().clear();
root.getChildren().add(choices);
});
Platform.setImplicitExit(true);
stage.setScene(new Scene(root));
stage.show();
}
}
The design is dynamic. I have a list of values received from a server. This is used to create and place ComboBox on a grid. When the user changes a selection in a ComboBox it receive a new list of values from the server. This list may still contain values that corresponds to existing nodes in the grid. They are reused rather than re-created.
Just to not loose reason and solution posted as comment to the deleted answer by sillyfly (post your own and I'll delete this :)
A little guess as to the underlying cause/issue - the change causes the ComboBox to disappear while its list (which is technically a different stage) is showing. My guess is that leaves it in an indefinite state where it thinks the list is still showing, but it never hides so it doesn't reset. In this case, maybe calling ComboBox::hide will also work
This assumption is correct as you can see if you change the selection by keyboard (in which case the dropdown is not open): the combo is still accessible by keyboard and mouse. So hiding the dropdown before removing indeed is the solution.
In code (the simplified example in the Michael's edit)
public class ReaddFocusedCombo extends Application {
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
// guess by sillyfly: combo gets confused if popup still open
choices.hide();
root.getChildren().clear();
root.getChildren().add(choices);
// suggested in answer: working but then the choice isn't focused
//root.requestFocus();
// doesn't work
// choices.requestFocus();
});
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Update: a little search in the bug parade turned up a similar misbehaviour on adding a showing combo which was fixed on initial attaching to a scene, but missed the dynamic use case. Filed a new issue for the latter.

JavaFx how to avoid creating one huge controller

I have an app in JavaFX, which has main scene with menu and toolbar, and smaller scenes, which are injected into this main scene, after one of menu buttons are being pressed.
Now, HomeCntroller is responsible for either scene components: Home Scene (with toolbar and menu), and injected scene. This leads me to create massive, huge and very unprofessional controller if number of injected scenes is more than one.
How to split controller responsibility?
Now my Controller looks like this:
changeDashboardPane method injects smaller Pane into my main HomePane.
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired) )
public class HomeController extends AbstractController {
private static final Logger LOG = Logger.getLogger(HomeController.class);
private final BudgetProfileService budgetProfileService;
#FXML
private Label usernameLabel;
#FXML
private ComboBox<String> budgetProfilesComboBox;
#FXML
private AnchorPane dashBoardPane;
#FXML
public void initialize() {
refreshUsernameLabel();
getAllBudgetProfiles();
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
private void refreshUsernameLabel() {
String username = UserAccountProvider.getLoggedUser().getUsername();
usernameLabel.setText(username);
}
private void getAllBudgetProfiles() {
List<String> budgetProfileNames = budgetProfileService.getAllBudgetProfileNames();
if (!budgetProfileNames.isEmpty()) {
budgetProfilesComboBox.getItems().clear();
budgetProfilesComboBox.getItems().addAll(budgetProfileNames);
}
}
#FXML
public void handleFinancesButtonAction() {
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
#FXML
public void handlePeriodButtonAction() {
changeDashboardPane(PaneFactoryKeys.PERIOD_PANE);
}
#FXML
public void handleStatisticsButtonAction() {
changeDashboardPane(PaneFactoryKeys.STATISTICS_PANE);
}
#FXML
public void handleSettingsButtonAction() {
changeDashboardPane(PaneFactoryKeys.SETTINGS_PANE);
}
private final void changeDashboardPane(String paneFactoryKey) {
double injectedPanePosition = 0.0;
Pane paneToChange = getPaneFromFactory(paneFactoryKey);
dashBoardPane.getChildren().clear();
AnchorPane.setTopAnchor(paneToChange, injectedPanePosition);
dashBoardPane.getChildren().add(paneToChange);
}
}
To get this more clear, screens:
without injected second pane
with injected second pane
Any ideas guys?
I would recommend you to divide your main scene in smaller ones, for example you can have a tools scene, a header scene, a content scene and so on. Then you should have one controller for every scene.
After that I would use a publisher-subscriber pattern to deal with behaviors, like when you press a button on settings scene, it triggers an event that other scenes listen to and then they handle it changing their state accordingly.
I hope it was clear and can help!
Create multiple controllers , multiple FXML files - to continue on my answer that i provided you before, JavaFX how to inject new FXML content to current Scene each of those views that have separate fxml file also has
fx:controller="appplication.ExampleViewController"
attached to it.So what you do is create main controller as was mentioned , that is basically the FRAME CONTAINER that encapsulates controls to change your dynamic container.If your application is really ui rich and have a lot of functionality in one controller , you can break down your view even further:
For instance take out menu and put it into separated controller , and insert it into your main view with main controller
/same way as in method setView()/
, what you are doing is just taking it away to keep controller code smaller, YOU DONT DECREASE/INCREASE SCENE GRAPH THIS WAY, doesnt have a drawback its just a personal preference.
You gonna end up with more fxml files and controllers in the end.Its all the same thing as from your previous question there is no additional code needed you can actually reuse what was already provided.
Data between controllers are passed thru MODEL. - look more into MVC dont work with application data in controllers only care about view or passing them from/into model
To avoid a huge contoller class, as I am using multiple tabs, I split the tabs to single java files.
My solution was to create a cascade of classes:
Base: Containing all defs for FX types
Tab1 extends Base: Tab one implementation
Tab2 extends Tab1: Tab two implementation
Controller extends Tab2 implements Initializable: Implements initialize(URL url, ResourceBundle resourceBundle)
Important:
Any accessed object must be definded in the current tab or before.
Any Objects in Base are available in Controller whereas no object of Controller is accessable in Base, Tab1 or Tab2.
Feel free to add your opinion as comment or submit a improvement.

Linking content from different fxml files

I have a JavaFx project I created using SceneBuilder.
I am also using a Guice plugin architecture.
I have one .fxml file that has a pane that I want to be the the content of another .fxml file.
Is there any easy way to link .fxml content from one file to another?
I have not used fx.guice plugin architecture before. Is there an easier way to this with plugin control?
Thanks!!
This was a big problem for us since we also are using Guice and JavaFX.
tl;dr I've attached some code at the bottom that we've been using for a year and a bit now without issue.
edit: I should've mentioned we did all this stuff before fx,guice existed, so this exists entirely outside that, and I probably should be using it.
Yes, but you will have to modify the view tree from java, and you must instantiate two controllers. If you're willing to let the fxml loader instantiate your controllers (we're not, more in a sec), then you simply need to have code along the lines of
solution 1:
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which isn't nice because it means the FX loader will try to create your controller for you, but you probably want guice to do that
luckily you can call setController (or setControllerFactory) to leverage guice, so now we have
solution 2:
#Inject private OuterController outerController
#Inject private InnerController innerController
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
//using the setControllerFactory instead of the setController
//means you can still declare the controller type in FXML
//which is good for our IDE intelliJ and general readability
fxmlLoader.setControllerFactory(() -> outerController);
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
fxmlLoader.setControllerFactory(() -> innerController);
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which is better but requires a 3rd party to load your components. What you really want, a le dependency-injection, is to have the child view be resolved first and as part of the resolution of the parent view.
For us, this brings us to
Solution 3:
class OuterController{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
loader.load();
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public InnerController(FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourInnerVIew.fxml"));
loader.load();
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
And finally, in the name of 'convention over configuration', we created a few classes (attached below) that attempt to 'automatically' find the view by reflecting on the controllers name (eg OuterController) and assuming that it will find an FXML view view in the same directory as the controller's class file with the word controller replaced with view (eg OuterView.fxml)). We also leveraged a neat trick in super-ctor-order in java to allow us to have pre-setup FXML values.
So now we get:
solution4 :
class OuterController extends PreloadedFX{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#FXML Checkbox importantCheckbox;
#FXML Label importantLabel;
// because of 'PrealoadedFX' getting called first,
// you can actually inline initialize object constants
// like this
private final ObservableBooleanValue isSelected = importantCheckbox.selectedProperty();
// or using an initializer
{
int x = 4;
importantLabel.setText(importantLable.getText() + x);
}
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
super(loader);
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController extends PreloadedFX{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public OuterController(FXMLLoader loader){
super(loader);
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
you can get the source code for the PreloadedFX and View-By-Convention code here:
https://gist.github.com/Groostav/ff35eb2d19b348f2e25c
which is as elegant as I've been able to make this particular union of frameworks.
Hope that helps!

Categories