Purpose of EventDispatcher
As the name suggests, the purpose of an EventDispatcher is to dispatch an
Event. The way it does this is dependent on the implementation. However, the
standard implementation (internal to JavaFX) makes EventDispatcher a kind of
"collection" of EventHandlers. It is the responsibility of the EventDispatcher
to invoke the appropraite EventHandler at the correct time. What makes
an EventHandler "appropriate" depends on:
- If the
EventHandler was registered for the current phase of the event
dispatching cycle (capturing or bubbling)
- If the
EventHandler was registered for the current Event's EventType or
one of the supertypes (i.e. EventType.getSuperType())
Event Path (or "Chain")
If we focus on the scene-graph, when an Event is fired it starts at the top
of the scene-graph (the Window) and travels through the hierarchy to
the target (usually a Node, but at the very least an implementation of
EventTarget). This is the "capturing" phase of the event dispatch cycle. After
reaching the target it travels back up the scene-graph until it reaches the
Window again. This is the "bubbling" phase of the cycle.
Capture Phase
Window -> Scene -> Root Node -> Middle Node -> EventTarget
Bubble Phase
Window <- Scene <- Root Node <- Middle Node <- EventTarget
If at any step the Event is consumed (via Event.consume()) then it will not
be forwarded to the next step. This effectively stops the processing of the
Event at the desired step in the chain.
The way this path is computed is done by an implementation of
EventDispatchChain and the EventTarget. An EventTarget must implement
the following method:
EventDispatchChain buildEventDispatchChain(EventDispatchChain tail);
When an Event is fired it has a designated EventTarget. Since the
EventTarget will be at the bottom of the scene-graph the chain is
built from the bottom up. It does this by prepending an EventDispatcher to the
EventDispatchChain at each level in the hierarchy (using
EventDispatchChain.prepend(EventDispatcher)). This is where EventDispatcher
starts to comes in.
Each EventTarget usually has its own EventDispatcher associated with it. The
implementations of EventTarget in standard JavaFX (Window, Scene, Node,
MenuItem, etc...) provide their own implementations of EventDispatcher. In
this regard you don't have to worry about how to use EventDispatcher. You don't
even use it directly. Rather, you add EventHandlers via the
[add|remove]EventHandler and [add|remove]EventFilter methods as well as
he various onXXX properties.
When buildEventDispatchChain is called on say, a Button, the Button prepends
its EventDispatcher to the given EventDispatchChain. It then calls
buildEventDispatchChain on its Parent if it has one. This continues up to
the root Node of the Scene. The root Node calls buildEventDispatchChain
on said Scene which, after prepending its EventDispatcher, does the same on
the Window it is attached to.
At this point the EventDispatchChain is fully built and is ready to process
the Event. If not obvious yet, the EventDispatchChain is fundamentally just
a "stack" of EventDispatchers. In other words, it is a highly specialized
java.util.Deque but without actually extending that interface.
Note: EventDispatchChain also provides an append(EventDispatcher) method
for the situations where prepending is the wrong operation.
Dispatching The Event
Once the EventDispatchChain is fully built it is time to actually dispatch the
Event. This is done by calling this method on the EventDispatchChain:
Event dispatchEvent(Event event);
This has the EventDispatchChain get (pop) the first EventDispatcher on
the stack and call its method:
Event dispatchEvent(Event event, EventDispatchChain tail);
Side Note: Unfortunately EventDispatcher's method has a similar signature to
EventDispatchChain.dispatchEvent(Event) which may cause confusion.
Side Note #2: The tail will be the same EventDispatchChain throughout the
entire process.
This is where the EventDispatchers are actually used. Here is the algorithm
used by each EventDispatcher as defined by the internal
com.sun.javafx.event.BasicEventDispatcher class (Java 10 source code):
@Override
public Event dispatchEvent(Event event, final EventDispatchChain tail) {
event = dispatchCapturingEvent(event);
if (event.isConsumed()) {
return null;
}
event = tail.dispatchEvent(event);
if (event != null) {
event = dispatchBubblingEvent(event);
if (event.isConsumed()) {
return null;
}
}
return event;
}
The steps are:
- Dispatch the
Event for the capturing phase
- This invokes all the
EventHandlers that were added as a filter
- Consuming an
Event here will not stop the dispatching of the Event in
this step; but will stop the Event from being processed in later steps
- If the
Event is consumed, return null, else forward the Event to
tail and wait for it to return
- If
tail.dispatchEvent doesn't return null then dispatch the Event for
the bubbling phase
- If the returned
Event was null that means the Event was consumed
somewhere down the chain and no more processing is to be done
- This invokes all the
EventHandlers that where added as a handler (which
includes the ones added via the onXXX properties)
- As with step #1, consuming an
Event here does not stop the processing of
this step; but will stop the Event from being processed in later steps
- If the
Event is consumed return null, otherwise return the Event
- As with
EventDispatchChain, returning null means the Event has been
consumed and processing of the Event must stop
This is done for each EventDispatcher in the EventDispatchChain. The call
to tail.dispatchEvent is bascially a "recursive" operation (without it being
"real" recursion). In effect this code walks down the stack (the calls to
tail.dispatchEvent) and then walks back up the stack (when tail.dispatchEvent
returns). And each "link in the chain" does processing before the "recursive"
call (the capturing phase) and after the "recursive" call returns (the bubbling
phase).
Notice here, though, that at each step it is the EventDispatcher that is
actually invoking each of the appropriate EventHandlers. This is how an
EventDispatcher is used.
Implementing Your Own EventDispatcher
When extending a class that already implements EventTarget then you should only
create your own EventDispatcher when absolutely needed. If your goal is to
control if an Event reaches a certain EventTarget then your first choice should
be to consume the Event at the appropriate place (mentioned by Jai in the
comments). If you want to alter something about the path of the Event then you
might need to provide your own EventDispatcher. However, due to the closed
nature of the internal EventDispatcher implementations, coupled with the fact
that the EventDispatcher interface is limited, you will probably be restricted
to wrapping the original EventDispatcher in your own implementation and delegating
when necessary. I've seen this done in other people's code (might have even seen
it in JavaFX itself) but I can't remember the code well enough to give you
examples of this.
If you are creating your own EventTarget from scratch then you will have to
implement your own EventDispatcher. Some things to keep in mind if you do
need your own implementation:
- It must only invoke the
EventHandlers registered for the current phase (if
there are to be phases)
- It must only invoke the
EventHandlers registered for the Event's EventType
and said EventType's supertypes
- It must forward the
Event to the tail EventDispatchChain
- It must only return
null if the Event has been consumed
- It must be capable of concurrent modification by the same
Thread
- The reason for this is because an
EventHandler may remove itself or
add/remove another EventHandler while its handle method is executing. This
will take place while the EventDispatcher is iterating the EventHandlers
in some way.
- It must "fix the source" of the
Event. In the standard JavaFX implementation
each EventDispatcher updates the source of the Event to be the Object
associated with that EventDispatcher (such as the Node). This is done in
subclasses of the above mentioned com.sun.javafx.event.BasicEventDispatcher.
The method to "fix the source" is Event.copyFor(Object, EventTarget)
- Note: I'm not sure if this is actually part of the contract and may not be
necessary if your implementation doesn't require it.