Thanks for posting your solution Michael. Glad you have something which works for you.
Your idea to centralize the navigation control is similar to one that I had when I created this
small framework for navigation of FXML based scenes.
Your solution uses object passing whereas mine uses static data, but the overall effect seems similar.
A more extensive solution is that presented by Angela Caicedo for
multiple scene navigation in JavaFX.
I still think that the dependency injection based approach I mentioned earlier such as Afterburner is cleaner than the either the object passing or static data approach, but I understand that concepts like dependency injection can sometimes seem strange to people.
The following is going to get a bit dense and may be difficult to understand. It's just advice, so if it confuses more than it helps, please ignore it for now and you always come back and study things like this later.
Some advice on your implementation is to concentrate a little more on design, try to stick to the
Single Responsibility Priniciple (that is why I have a separate Navigator class to manage navigation rather than overloading the Application class to perform navigation tasks).
Try not to do things which add additional, potentially unnecessary framework boilerplate code to all your classes - e.g. SceneBuilder has a function where it generates a template controller class from a FXML class, if you write your app such that you basically stick to that template and just add some more additions for the functionality you require then it makes switching between the graphical design tool and your app code easier.
Be careful with your approach of having a lot of
globally accessible state in your application (for the reasons mentioned in the linked article).
Your approach limits you to just a single Controller class for the system, whereas it might be best to segregate different controllers for different parts of the system where each controller is just encapsulating the UI elements for a smaller part of the whole system.
What you can have is a kind of model (or view-model in an
MVVM architecture nomenclature) based system, potentially based on the property binding and listeners where the model or view-model knows nothing of the associated GUI components but instead provides listeners. The controller classes are set with the view-model references either passed in to them via constructors or methods or via dependency injection. This provides a
separation of concerns whereby the controllers do the binding of the model to the UI and the models themselves react to changes via property listeners, potentially calling into functions on domain objects which take actions based upon the data (for example your file actions). An example of a listener driven approach for providing a skinnable application is this
tic-tac-toe game. The way I described things in this paragraph is pretty much the same as the internal
JavaFX control architecture and is what gives the controls a lot of their flexibility to separate behavior styling and functionality, such that you can completely re-skin a control (e.g. a ComboBox can have a traditional mouse driven drop down control on a desktop or a rolodex style touch driven control on a mobile interface) but the API to use the control never changes.
The disadvantage of the indirect property change listener approach to programming is that it can make some things more difficult to understand as a lot of operations are driven by listeners invoked as side-effects of data changing (so it's a tradeoff between using the abstract model approach or just embedding all the logic and data you need into the controller). So the lead developer for JavaFX controls doesn't advise that users write controls for all elements of their application - only those which need compartmentalization or are good candidates for standardizing in a library. Nevertheless it is good to really understand these kind of concepts so that you can make good decisions on when to apply them as you can still make use of some of the concepts for the controls development without actually inheriting from the Control class.
One great thing about using concepts like the single responsibility principle and separation of concerns is that it makes your application much easier to
test. When you have a lot of shared state and mixed responsibilities it is very difficult to unit test functionality in isolation.
The event driven concepts for property change events also set your application up
reactive programming model which is the basis for libraries such as
ReactFX (although I would still tend treat reactive programming with JavaFX as a bit more of an experimental thing for now rather than something your would generally use in production code).