This may be a little controversial, but I have come to the conclusion that trying to implement a strict MVC separation with a separate Controller class may not be the best way to design a JavaFX desktop application.
How I've approached it is to build rich domain models that have most of the "controller" aspects built in. The application is layered into Model, View, and UI component classes.
Model class are the domain objects - they model your business or application domain. They implement domain-specific methods for modifying the model (adding child items, changing status codes etc). This could be thought of as the domain-specific controller functionality.
The UI component classes extend or encapsulate a Node and are scenegraph objects that accept a domain object and understand how to display it. They may also provide single- or bi-directional binding so that model updates automatically update the display, and edits in the UI automatically update the model.
The View classes implement the main "tab" or "window" aspects of the UI. They hold a collection of UI components, and lay them out in groups or subgroups. The view is responsible for accepting the domain object to work with, and then binding/wiring up all the UI components necessary. It may also have some limited awareness of the domain model - e.g., you might need to show or hide some UI components based on model state. So this handles controller functionality related to the UI presentation.
Menu commands, shortcut keys and context menus bind to "Action" classes that represent commands that can be issued. These are a separate layer of functionality that can inspect the View to determine current context and the domain object being used, and then issue commands to the model.
For an example :-
You have a "sales order" object that models a customer order. The UI exposes a drop-down to select discount percent. That drop-down is bound to a "selectedDiscount" property on the sales order object. Within the model there is a listener that is called when that property is modified, and triggers a recalculation of the order price.
The UI has a table bound to the order lines, with fields observing the line price properties on the sales order. When the user changes the drop-down, it triggers a price recalculation, which updates the model, which updates the displayed prices.
There are lots of alternative designs, and some might fit better that others depending on your particular application.
I'm currently ~2 years into developing a fairly large-scale and complex JavaFX desktop application that currently has something like 500 classes and a few dozen tabs/windows/dialogs in the UI, so this is just based on my personal experience. I experimented with a number of designs - event-bus based, with centralised controllers; React-style; and using Presentation Model classes, and they all have various strengths and weaknesses. The approach I've outlined above has worked well for me so far - but is a bit more complex in reality than the very abbreviated description given here. It's the simplest approach I've worked out that has enough abstraction to be useful and avoid duplication and make it easy to refactor, while not adding so much abstraction overhead that it just gets in the way.
Unfortunately I have not yet found any really good case studies on the design and structure of JavaFX applications to show what sort of approaches work best.
Edit: Additional comments on event handling
Event handling - without getting too bogged down in detail, I either have event handlers within the UI components that directly call methods on the model, or Action classes that retrieve the current domain object from some context and then call methods on it.
Example 1 - using simple event handlers. Take a UI component that shows a list of items. The UI component has a list view control and some up/down buttons for moving items. In Button.setOnAction() I reference a handler method within the UI component. That method looks at the list, gets the current selected row, the calls a method on the model; something like
The model works out what to change, and updates it's list of items. The list view is bound to the list, so the displayed items automatically change.
Obviously, this is coupling the UI component fairly closely to the model - but for my usage that's an acceptable tradeoff, since the UI code needs to understand the model in order to display it appropriately.
Example 2 - using Actions. This is for when you want to be able to raise an event from multiple locations. E.g., you have a command to delete an order line. This can be invoked from a menu command, a toolbar button, a hotkey, or a popup right-click menu on the order table. In all cases the effect needs to be the same - invoking a method on the domain object to remove a row.
In this case I have an Action object, e.g., ActionDeleteOrderLine. The menu has an instance bound to it, as does the toolbar button. When the menu command is invoked, it calls the event handler method on the action. This event handler then gets the current selection context (ie., the domain object the user is currently working with/has selected), and invokes the appropriate method on the domain object.