On 13/01/2026 00:43, Andy Goryachev wrote:
>
> The reason I mentioned #2 is that it is somewhat relevant to the
> discussion, as in "why do we need to write custom dispatchers at all?"
> There should be only two methods, in my opinion, one that dispatches
> an event that bubbles up (with filters and handlers), and one that
> sends an event to a single target Node and nothing else.
> <rant>Somehow, Swing got the Events right - it manages to dispatch
> one (1) event in total, and the dispatching stops once the event is
> consumed. The FX decided it needed to reinvent the wheel and leave
> multiple booby traps in the process.</rant>
Although I agree that how FX solved events is sub-optimal, there is a
real need here to communicate to the EventHandler on which object it
resides. EventHandler instances are expensive when you need to attach
one to every Cell in a TableView, and so to re-use a single instance,
you need to know which Cell the event applies to. The source field
(which is supposed to be constant) has been abused for this, making
events non-constant requiring cloning before they can be dispatched to
their final target. This cloning then caused the "isConsumed" problem.
Perhaps we should just make the source field mutable as well, so the
cloning isn't needed.
The solution to this problem at the time should not have been to modify
events, but to have made event handlers be BiConsumers, with the Event
**and** Node being passed to the callback (and a "convenience" method
that delegates to the BiConsumer variant that accepts only
Consumer<Event> -- we may be able to still do this...)
>
> This isn't exactly rocket science, we should be able to figure
> something out. Maybe there is another option that will satisfy everyone?
I think the issue isn't so much in event dispatching, but in the
Skin/Behavior system itself. Skin/Behaviors in FX is like giving root
access to every user on your system. Sure it is convenient that
everyone can do whatever they want, and as long as everyone behaves,
everything works great. However one malicious user can interfere with
others or leave behind hooks that later come to bite you.
Controls are HOSTS for Skins and Behaviors. Skins and Behaviors are
clients. They should be restricted to a very specific subset of
functionality that benefits the host and is predictable for the host:
- Skins get ownership of the children list of the Control; while a Skin
is installed, the host should not be allowed to make modifications
- Skins can monitor properties for changes but this should never lead to
a direct observable change on the main control that a subsequent
installed listener may observe; in other words, listener order should be
irrelevant for what the Skin does in order to share the listener
infrastructure without interference. Skins are free to directly take
action on the children (which they own exclusively), just not on the
main control; such actions should instead be deferred, usually by
requesting a layout (this is usually already the case, but it is good to
make this explicit so we can decide what a Skin is doing is a bug or not).
- Behaviors can react to events at the lowest precedence, and
exclusively only take action when receiving an event; this means that
blocking all events will automatically mean the Behavior no longer does
anything, but also that selectively blocking events allows some control
over Behaviors
- Behaviors can co-modify properties on the Control, but this should be
clearly documented; controls are free to restrict this set (ie. a
Behavior has no business modifying the "wrapText" property, or things
like layout properties -- most often they do their work through pseudo
class changes and modifying the value a control represents).
That should really be all that is needed for a functioning Skin/Behavior
system; no need for root access.
Of course, root access to the Control is a ship that has sailed a long
time ago; but that doesn't mean we can't introduce a client API for
Skins/Behaviors. All that really takes is passing an object to the
Skin/Behavior when it is installed. This object is an interface with
which the Skin/Behavior can do their work. Should they choose to not
circumvent this API, and do all their work through this API, they can
remove all their clean-up code, as the Control can now do this
automatically. This will greatly simplify skins, and remove a whole
avenue of potential bugs.
All work done through this API can be monitored by the Control. The
control can:
- Track what is installed (for later clean-up)
- Reject installation of listeners/handlers it doesn't want to expose
- Ensure that event handlers are installed at lowest precedence. This
can be kept internal, so many solutions are possible: separate lists,
default event handlers (internal API), priorities, etc.
Everything you'd expect a host Control to be able to do, including
forcefully removing all traces of a previously installed Skin, and
disallowing it further access should it attempt to use the API again
after a new Skin is installed. That's however not a requirement; all
we'd need is that interface, and encourage Skins/Behaviors to use it.
Correctly behaved Skins/Behaviors then get all the benefits, and will
stop interfering with user code. This means of course modifications to
existing skins, but it is mostly in their registration logic (which I
think we modified like 5 times already).
The minimum API needed can be fairly small, and does not need to include
accessors for every property and handler with some smart signatures.
For example:
<T, P extendsReadOnlyProperty<T>> voidaddListener(Function<C, P>
supplier, Consumer<T> subscriber)
Allows installation of a listener by doing:
api.addListener(Slider::minProperty, v -> { ... });
In this way we can isolate and track what Skins/Behaviors are doing,
ensure they don't interfere with user operations on the Control and also
ensure guaranteed clean-up (if they refrain from accessing the Control
directly).
--John