This project provides a better mechanism for defining and overriding event handlers (e.g. keyboard shortcuts) for JavaFX. Such mechanism, also known as InputMap API, was considered as part of JEP 253 (see also JDK-8076423), but was dropped. (I guess I was the most vocal opponent of the proposal (link to discussion thread).)
Use cases in this section focus on expressivity of event matching, i.e. expressing what events should be handled.
The task is to add handlers for the following key combinations to Node node
:
Key Combination | Comment | Handler |
---|---|---|
Enter | no modifier keys pressed | enterPressed() |
[Shift+]A | optional Shift, no other modifiers | aPressed() |
Shortcut+Shift+S | saveAll() |
Nodes.addInputMap(node, sequence(
consume(keyPressed(ENTER), e -> enterPressed()),
consume(keyPressed(A, SHIFT_ANY), e -> aPressed()),
consume(keyPressed(S, SHORTCUT_DOWN, SHIFT_DOWN), e -> saveAll())
));
In some situations it is desirable to bind multiple different events to the same action. Task: Invoke action()
when a Button
is either left-clicked or Space-pressed. If these are the only two events handled by the button, one handler for each of MOUSE_CLICKED
and KEY_PRESSED
event types will be installed on the button (as opposed to, for example, installing a common handler for the nearest common supertype, which in this case would be InputEvent.ANY
).
Nodes.addInputMap(button, consume(
anyOf(mouseClicked(PRIMARY), keyPressed(SPACE)),
e -> action()));
Handle text input, i.e. KEY_TYPED
events, except for the new line character, which should be left unconsumed. In this example, echo the input to standard output.
Nodes.addInputMap(button, consume(
keyTyped(c -> !c.equals("\n")),
e -> System.out.print(e.getCharacter())));
Assume the following custom event declaration:
class FooEvent extends Event {
public static final EventType<FooEvent> FOO;
public boolean isSecret();
public String getValue();
}
The task is to print out the value of and consume non-secret Foo
events of node
. Secret Foo
events should be left unconsumed, i.e. let to bubble up.
Nodes.addInputMap(node, consume(
eventType(FooEvent.FOO).unless(FooEvent::isSecret),
e -> System.out.print(e.getValue())));
Use cases in this section focus on manipulating input mappings of a control, such as overriding mappings, adding default mappings, intercepting mappings, removing a previously added mapping, etc.
First install a handler on node
that invokes charTyped(String character)
for each typed character. Later override the Tab character with tabTyped()
. All other characters should still be handled by charTyped(character)
.
Nodes.addInputMap(node, consume(keyTyped(), e -> charTyped(e.getCharacter())));
// later override the Tab character
Nodes.addInputMap(node, consume(keyTyped("\t"), e -> tabTyped()));
The Control
might have installed a Tab-pressed handler for Tab navigation, but you want to consume all letter, digit and whitespace keys (maybe because you are handling their corresponding key-typed events). The point here is that the previously installed Tab handler is overridden even if it is more specific than the letter/digit/whitespace handler.
Nodes.addInputMap(node, consume(keyPressed(TAB), e -> tabNavigation()));
// later consume all letters, digits and whitespace
Nodes.addInputMap(node, consume(keyPressed(kc -> kc.isLetterKey() || kc.isDigitKey() || kc.isWhitespaceKey())));
It has to be possible to add default (or fallback) mappings, i.e. mappings that do not override any previously defined mappings, but take effect if the event is not handled by any previously installed mapping. That is the case for mappings added by skins, since skin is only installed after the user has instantiated the control and customized the mappings.
The task is to first install the (custom) Tab handler (tabTyped()
) and then the (default) key typed handler (charTyped(c)
), but the custom handler should not be overridden by the default handler.
// user-specified Tab handler
Nodes.addInputMap(node, consume(keyTyped("\t"), e -> tabTyped()));
// later in skin
Nodes.addFallbackInputMap(node, consume(keyTyped(), e -> charTyped(e.getCharacter())));
Suppose the skin defines a generic key-pressed handler, but the user needs Tab-pressed to be ignored by the control and bubble up the scene graph.
// ignore Tab handler
Nodes.addInputMap(node, ignore(keyPressed(TAB)));
// later in skin
Nodes.addFallbackInputMap(node, consume(keyPressed(), e -> handleKeyPressed(e)));
When changing skins, the skin that is being disposed should remove any mappings it has added to the control. Any mappings added before or after the skin was instantiated should stay in effect. In this example, let's add handlers for each of the arrow keys and for mouse move with left button pressed. Later, remove all of them, but leaving any other mappings untouched.
// on skin creation
InputMap<InputEvent> im = sequence(
consume(keyPressed(UP), e -> moveUp()),
consume(keyPressed(DOWN), e -> moveDown()),
consume(keyPressed(LEFT), e -> moveLeft()),
consume(keyPressed(RIGHT), e -> moveRight()),
consume(
mouseMoved().onlyIf(MouseEvent::isPrimaryButtonDown),
e -> move(e.getX(), e.getY())));
Nodes.addFallbackInputMap(node, im);
// on skin disposal
Nodes.removeInputMap(node, im);
Suppose we have a number of input mappings whose handlers share some common at the end. We would like to factor out this common code to avoid repetition. To give an example, suppose each move*()
method from the previous example ends with this.moveCount += 1
. Let's factor out this common code to a single place. (Notice the ifConsumed
.)
InputMap<InputEvent> im0 = sequence(
consume(keyPressed(UP), e -> moveUp()),
consume(keyPressed(DOWN), e -> moveDown()),
consume(keyPressed(LEFT), e -> moveLeft()),
consume(keyPressed(RIGHT), e -> moveRight()),
consume(
mouseMoved().onlyIf(MouseEvent::isPrimaryButtonDown),
e -> move(e.getX(), e.getY()))
).ifConsumed(e - { this.moveCount += 1; });
Nodes.addFallbackInputMap(node, im);
Consider a control that defines m input mappings and that there are n instances of this control in the scene. The space complexity of all input mappings of all these controls combined is then O(n*m). The goal is to reduce this complexity to O(m+n) by having a shared structure of complexity O(m) of the m input mappings, and each of the n controls to have an input map that is a constant overhead (O(1)) on top of this shared structure.
This is supported by package org.fxmisc.wellbehaved.event.template
.
The shared structure is an instance of InputMapTemplate
.
The API for constructing InputMapTemplate
s very much copies the API for constructing InputMap
s that you have seen throughout this document, except the handlers take an additional argument—typically the control or the "behavior". A template can then be instantiated to an InputMap
, which is a constant overhead wrapper around the template, by providing the control/behavior object.
Example:
static final InputMapTemplate<TextArea, InputEvent> INPUT_MAP_TEMPLATE =
unless(TextArea::isDisabled, sequence(
consume(keyPressed(A, SHORTCUT_DOWN), (area, evt) -> area.selectAll()),
consume(keyPressed(C, SHORTCUT_DOWN), (area, evt) -> area.copy())
/* ... */
));
TextArea area1 = new TextArea();
TextArea area2 = new TextArea();
InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area1);
InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area2);
Notice that INPUT_MAP_TEMPLATE
is static
and then added to two TextArea
s.
Maven artifacts are deployed to Maven Central repository with the following Maven coordinates:
Group ID | Artifact ID | Version |
---|---|---|
org.fxmisc.wellbehaved | wellbehavedfx | 0.3 |
dependencies {
compile group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3'
}
libraryDependencies += "org.fxmisc.wellbehaved" % "wellbehavedfx" % "0.3"
Download the JAR file and place it on your classpath.
License: BSD 2-Clause License
API documentation: Javadoc