JavaFX KeyEvent propagation order - java

I want to listen to some KeyEvent in my scene, say KeyCode.ESCAPE(close the scene when pressed).
scene.addEventHandler(KeyEvent.ANY, event -> {
if (event.isConsumed())
return;
switch (event.getCode()) {
case ESCAPE:
stage.hide();
event.consume();
break;
default:
break;
}
});
Now, the nodes inside the scene could have listened to ESCAPE too.
// ....
someOtherNode.addEventHandler(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
// do stuff
e.consume();
}
});
// ....
How do I make sure that the KeyEvent will be consumed from the node and not the scene?
Based on the diagram from Oracle, A workaround would be adding a dummy Node at the end of the Node hierarchy that listens to KeyCodes
But is there a better solution, like inverting the propagation route?
EDIT:
The use case:
A popup-like node that blocks other nodes would need to listens to the ESC key or focusProperty() so that it can close itself.

There's two ways you can affect events:
Use the Node.addEventFilter(...) method to register a filter. A filter will execute on the capturing phase of the event (as the window is getting more specific, determining which Nodes should get the event).
Use the Node.addEventHandler(...) method to register a handler. The handler will execute starting at the most specific node found in the capturing phase, heading down until it is consumed.
So in the capturing phase, a stack is created. Starting with the window (topmost parent), each node that this event could potentially execute on is added to the stack (ending with the bottom most child). A filter can interrupt this process, or just execute an event during this process.
In the bubbling phase, the event handlers will start firing from the top of the stack (created in the capturing phase) until the stack is empty or the event is consumed.
In your case, you really shouldn't have anything to worry about. If any node cares about processing the "ESC" event, they will do so in the bubbling phase (and they should consume the event to prevent further processing). You can see this behavior in the ComboBox. If they don't care, it will bubble up to your Scene and that handler will execute. Just make sure any custom code you create that processes an "ESC" press also consumes that event.
For more information, there is a explanation and tutorial here: http://docs.oracle.com/javafx/2/events/jfxpub-events.htm
And here is some sample code demonstrating the Escape functionality. Pressing ESC while focused on the ComboBox will not cause the application to close, while it will close with the other controls.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.converter.DefaultStringConverter;
public class FXEventFiltering extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//All the controls are added here
VBox box = new VBox();
ComboBox<String> dropdown = new ComboBox<>();
TextField field = new TextField();
CheckBox check = new CheckBox("Check");
RadioButton radio = new RadioButton("Radio!");
TextArea area = new TextArea();
TableView<String> table = new TableView<String>(FXCollections.observableArrayList(new String[]{"one","two"}));
TableColumn<String, String> tc = new TableColumn<String, String>("Column1");
tc.setEditable(true);
tc.setCellFactory(TextFieldTableCell.<String,String>forTableColumn(new DefaultStringConverter()));
tc.setCellValueFactory(new Callback<CellDataFeatures<String,String>, ObservableValue<String>>(){
#Override
public ObservableValue<String> call(CellDataFeatures<String, String> arg0) {
return new SimpleStringProperty(arg0.getValue());
}});
table.getColumns().add(tc);
box.getChildren().addAll(dropdown, field, check, radio, area, table);
//Setting up your scene
Scene scene = new Scene(box);
stage.setScene(scene);
scene.addEventHandler(KeyEvent.ANY, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
System.out.println("KEYS!" + event.getEventType().getName());
switch (event.getCode()) {
case ESCAPE:
System.out.println("Escape!");
stage.hide();
event.consume();
break;
default:
break;
}
}
});
box.requestFocus(); // Removing default focus
stage.show();
}
}

Maybe you could loop over all nodes after catching the event in the scene to find out which node has actual focus? Then you could call node method to close?

Related

Invoke a ComboBox's enter key/action event handler regardless of value property change

With an editable ComboBox, is there any way to have the ENTER key event or action event handler occur regardless of whether or not the Combobox's value property has changed?
I essentially would like to have the same behaviour in a ComboBox's TextField on pressing the ENTER key as it occurs for a TextField.
What I Have Tried
My initial thought was to simply use setOnAction for a ComboBox; however, according to the documentation for it:
The ComboBox action, which is invoked whenever the ComboBox value property is changed. This may be due to the value property being programmatically changed, when the user selects an item in a popup list or dialog, or, in the case of editable ComboBoxes, it may be when the user provides their own input (be that via a TextField or some other input mechanism.
Thus, by using setOnAction, the event handler only occurs if:
The value property is changed via a change in selection from the
drop down OR
The value property is changed via user-input (ie: it does not occur
if the user does not type anything and presses ENTER nor does it occur
if the user does not change their input after the event handler has run
once and they press ENTER).
Also, neither using setOnAction on the ComboBox's TextField nor using setOnKeyPressed achieves the desired behaviour.
Below is an SSCCE to demonstrate:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage primaryStage) {
ComboBox<String> comboBox =
new ComboBox<String>(
FXCollections.observableArrayList("XYZ", "ABC"));
comboBox.setEditable(true);
comboBox.setValue(comboBox.getValue());
comboBox.setOnAction((event) -> System.out
.println("occurs on selection changes or text changes and ENTER key"));
comboBox.getEditor().setOnAction(
(event) -> System.out.println("this never happens"));
comboBox.getEditor().setOnKeyPressed((keyEvent) -> {
if (keyEvent.getCode() == KeyCode.ENTER)
System.out.println("this never happens either");
});
TextField tf = new TextField();
tf.setOnAction((event) -> System.out.println("always happens on ENTER"));
HBox hbox = new HBox(comboBox, tf);
Scene scene = new Scene(hbox);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
As a general way of finding out how eventing works, you could always add an eventfilter with Event.ANY and see what happens, e. g.:
comboBox.getEditor().addEventFilter(Event.ANY, e -> System.out.println(e));
The event gets fired, as can be seen in the console. So what you need is to add a filter for the key code like this:
comboBox.getEditor().addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ENTER) {
System.out.println( "Enter pressed");
}
});
Regarding your problem you can take a look at ComboBoxListViewSkin where you can see that the event is consumed.
private EventHandler<KeyEvent> textFieldKeyEventHandler = event -> {
if (textField == null || ! getSkinnable().isEditable()) return;
handleKeyEvent(event, true);
};
...
private void handleKeyEvent(KeyEvent ke, boolean doConsume) {
// When the user hits the enter or F4 keys, we respond before
// ever giving the event to the TextField.
if (ke.getCode() == KeyCode.ENTER) {
setTextFromTextFieldIntoComboBoxValue();
if (doConsume) ke.consume();
}
...
}
So in short: Use an EventFilter instead of an EventHandler.
You can read more about that in the Handling JavaFX Events documentation.

JavaFX 2 User Idle Detection

I'm trying to make a simple Java transaction app with JavaFX as UI.
What I want to do now is to detect user idle state from my application which has 1 primary stage and many scenes.
Example : if user idle for 3 minutes then go back to main menu.
I already try some examples on the web about how to detect JavaFX idle state, but what I found is always -one function idle state detection which is occuring all scenes- method which is (I think) dangerous for transaction app (ex : apps detect idle state in the middle of transaction process).
It's possible to detect user idle state on every single scene? how?
Thanks.
EDIT :
Examples that I already try :
http://tomasmikula.github.io/blog/2014/06/04/timers-in-javafx-and-reactfx.html
and
http://ochafik.com/blog/?p=98
I don't really understand the point you are making about transactional behavior. Transactions concern guarantees about the data, and your transactional behavior should be defined at the data level and should not be impacted by what is happening in the UI. In other words, your atomic behavior should complete or rollback even if the UI resets due to the user being idle.
Maybe this will help, though. (Note I used Java 8 code in these examples, but you can fairly easily make it JavaF 2.2 compliant if you need.) This follows Tomas Mikula's general approach in that it uses a Timeline to implement the idle check. I didn't use Tomas' FX Timer wrapper but you could certainly do so if you like. This class encapsulates a monitor for whether the user is idle. You can register any node (or scene) and type of event: if an event of that type occurs on that node (or scene), the user is determined not to be idle. If the specified time elapses without any registered events occurring, the provided runnable is executed (on the FX Application Thread). This gives you the flexibility to create multiple monitors, if needed, and to register one or more nodes with each.
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.util.Duration;
public class IdleMonitor {
private final Timeline idleTimeline ;
private final EventHandler<Event> userEventHandler ;
public IdleMonitor(Duration idleTime, Runnable notifier, boolean startMonitoring) {
idleTimeline = new Timeline(new KeyFrame(idleTime, e -> notifier.run()));
idleTimeline.setCycleCount(Animation.INDEFINITE);
userEventHandler = e -> notIdle() ;
if (startMonitoring) {
startMonitoring();
}
}
public IdleMonitor(Duration idleTime, Runnable notifier) {
this(idleTime, notifier, false);
}
public void register(Scene scene, EventType<? extends Event> eventType) {
scene.addEventFilter(eventType, userEventHandler);
}
public void register(Node node, EventType<? extends Event> eventType) {
node.addEventFilter(eventType, userEventHandler);
}
public void unregister(Scene scene, EventType<? extends Event> eventType) {
scene.removeEventFilter(eventType, userEventHandler);
}
public void unregister(Node node, EventType<? extends Event> eventType) {
node.removeEventFilter(eventType, userEventHandler);
}
public void notIdle() {
if (idleTimeline.getStatus() == Animation.Status.RUNNING) {
idleTimeline.playFromStart();
}
}
public void startMonitoring() {
idleTimeline.playFromStart();
}
public void stopMonitoring() {
idleTimeline.stop();
}
}
Here's a test. The "Start" buttons are perhaps stand-ins for logging in. The main UI has a tab pane with two tabs: each individual tab starts with its own "Start" button and then the main content has a label, text field, and button.
The tab contents each have a (short, for testing) idle monitor associated with them. Any event on the content of the tab will reset the idle monitor, but events outside of the tab content will not reset it. There's also a "global" idle monitor for the entire window which resets the whole UI after 30 seconds.
Note that the data is preserved: i.e. if you timeout due to the idle, any text you type in the text field is preserved properly. This is why I think the issue with "transactions" should not matter at all.
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class IdleTest extends Application {
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Parent mainUI = buildMainUI();
Scene scene = new Scene(root, 350, 150);
Parent startUI = buildStartUI(() -> root.getChildren().setAll(mainUI));
root.getChildren().add(startUI);
IdleMonitor idleMonitor = new IdleMonitor(Duration.seconds(30),
() -> root.getChildren().setAll(startUI), true);
idleMonitor.register(scene, Event.ANY);
primaryStage.setScene(scene);
primaryStage.show();
}
private Parent buildStartUI(Runnable start) {
Button button = new Button("Start");
button.setOnAction(e -> start.run());
StackPane root = new StackPane(button);
return root ;
}
private Parent buildMainUI() {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("One");
Parent tab1Content = buildTabUI("Tab 1");
Parent tab1StartContent = buildStartUI(() -> tab1.setContent(tab1Content));
tab1.setContent(tab1StartContent);
IdleMonitor tab1IdleMonitor = new IdleMonitor(Duration.seconds(5),
() -> tab1.setContent(tab1StartContent), true);
tab1IdleMonitor.register(tab1Content, Event.ANY);
Tab tab2 = new Tab("Two");
Parent tab2Content = buildTabUI("Tab 2") ;
Parent tab2StartContent = buildStartUI(() -> tab2.setContent(tab2Content));
tab2.setContent(tab2StartContent);
IdleMonitor tab2IdleMonitor = new IdleMonitor(Duration.seconds(10),
() -> tab2.setContent(tab2StartContent), true);
tab2IdleMonitor.register(tab2Content, Event.ANY);
tabPane.getTabs().addAll(tab1, tab2);
return tabPane ;
}
private Parent buildTabUI(String text) {
Button button = new Button("Click here");
button.setOnAction(e -> System.out.println("Click in "+text));
VBox content = new VBox(10, new Label(text), new TextField(), button);
content.setAlignment(Pos.CENTER);
return content ;
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX - Sequential Transition - playing back and forth (step by step)

I'm struggling with making an animation showing the searchin in Binary Search Tree in JavaFX.
The goal is to make a visualization of comparing the value of tree node with possibility to:
pause and play it any time
being able to play the animation backwards (to go at least one step back),
give a user an ability to play the animation step-by-step or whole at once.
The preview of visualization
My vision was to make a series of TranslateTransitions(TT) added in one SequentialTransition(ST). If the animation is marked as "step-by-step" the each TT pauses the whole ST in their OnFinished handler. However this kinda works only for going one-way.
My question is. What is the best approach to maintain going fluent and step-by-step animation in reverse direction ?
I was thinking about:
maybe making another sequence of inverse transitions (but how to tell
it from which step to continue ?)
somehow work with rate property ? is it possible to change it while the ST is running ?
Thank you very much for your answers.
In general, you can change the rate property of an Animation while it is in progress. The idea of using a SequentialTransition is appealing, but it doesn't work as easily as you might think. The problem arises when the sequential transition is paused at the boundary between two individual transitions: you don't have any way to tell which of the individual transitions is considered the current one (i.e. the next one or the previous one). So when you try to reverse the rate and play, the sequential transition can get confused and immediately think it's at the end of the one it's trying to play.
You might be able to hack this a little by using Animation.getCurrentTime() and Animation.jumpTo(...) to "nudge" the sequential transition a tiny amount in the correct direction before starting to play any step, but I think it's probably easier just to manage the individual transitions on their own instead of using a SequentialTransition.
Here's a simple example of using this technique to move a rectangle around:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ReverseSequentialTransitionTest extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Rectangle rect = new Rectangle(50, 50, 250, 150);
rect.setFill(Color.color(.5, .5, .1));
pane.getChildren().add(rect);
TranslateTransition ttForward = new TranslateTransition(Duration.seconds(1), rect);
ttForward.setFromX(0);
ttForward.setToX(400);
TranslateTransition ttDown = new TranslateTransition(Duration.seconds(1), rect);
ttDown.setFromY(0);
ttDown.setToY(100);
TranslateTransition ttBackward = new TranslateTransition(Duration.seconds(1), rect);
ttBackward.setFromX(400);
ttBackward.setToX(0);
TranslateTransition ttUp = new TranslateTransition(Duration.seconds(1), rect);
ttUp.setFromY(100);
ttUp.setToY(0);
List<Animation> transitions = Arrays.asList(ttForward, ttDown, ttBackward, ttUp);
IntegerProperty nextTransitionIndex = new SimpleIntegerProperty();
Button playButton = new Button("Play Forward");
playButton.setOnAction(event -> {
int index = nextTransitionIndex.get();
Animation anim = transitions.get(index);
anim.setOnFinished(evt -> nextTransitionIndex.set(index+1));
anim.setRate(1);
anim.play();
});
Button reverseButton = new Button("Play backward");
reverseButton.setOnAction(event -> {
int index = nextTransitionIndex.get()-1;
Animation anim = transitions.get(index);
anim.setOnFinished(evt -> nextTransitionIndex.set(index));
anim.setRate(-1);
anim.play();
});
// This is not really part of the answer to the current question, but the
// next three statements just disable the buttons when appropriate.
// This is a binding which is true if and only if any of the transitions are
// currently running:
BooleanBinding anyPlaying = createAnyPlayingBinding(transitions);
// Disable playButton if we are at the end of the last transition, or if
// any transitions are playing:
playButton.disableProperty().bind(
nextTransitionIndex.greaterThanOrEqualTo(transitions.size())
.or(anyPlaying)
);
// Disable reverseButton if we are at the beginning of the first transition,
// or if any transitions are currently playing:
reverseButton.disableProperty().bind(
nextTransitionIndex.lessThanOrEqualTo(0)
.or(anyPlaying));
HBox controls = new HBox(5);
controls.setAlignment(Pos.CENTER);
controls.getChildren().addAll(playButton, reverseButton);
BorderPane root = new BorderPane();
root.setCenter(pane);
root.setBottom(controls);
primaryStage.setScene(new Scene(root, 800, 400));
primaryStage.show();
}
private BooleanBinding createAnyPlayingBinding(List<Animation> transitions) {
return new BooleanBinding() {
{ // Anonymous constructor
// bind to the status properties of all the transitions
// (i.e. mark this binding as invalid if any of the status properties change)
transitions.stream()
.map(Animation::statusProperty)
.forEach(this::bind);
}
#Override
protected boolean computeValue() {
// return true if any of the transitions statuses are equal to RUNNING:
return transitions.stream()
.anyMatch(anim -> anim.getStatus()==Status.RUNNING);
}
};
}
public static void main(String[] args) {
launch(args);
}
}
In JDK 7, the event handler for the playButton looks like this:
playButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
final int index = nextTransitionIndex.get();
Animation anim = transitions.get(index);
anim.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent evt) {
nextTransitionIndex.set(index + 1) ;
}
});
anim.setRate(1);
anim.play();
}
});
and similarly for reverseButton. You will need to declare a couple of things as final as well. The createAnyPlayingBinding method is something like
private BooleanBinding createAnyPlayingBinding(final List<Animation> transitions) {
return new BooleanBinding() {
{
for (Animation transition : transitions) {
this.bind(transition.statusProperty();
}
}
#Override
protected boolean computeValue() {
// return true if any of the transitions statuses are equal to RUNNING:
for (Animation anim : transitions) {
if (anim.getStatus() == Status.RUNNING) {
return true ;
}
}
return false ;
}
};
}

How to stop arrow keys navigating controls

I'm trying to stop the arrow keys from navigating through the controls i have in my example. I'm not sure how to do this. Here is the example i have created:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class KeyEventTest extends Application{
private EventHandler<KeyEvent> keyEventHandler;
#Override
public void start(Stage stage) throws Exception {
// TODO Auto-generated method stub
Group root = new Group();
FlowPane f = new FlowPane();
Button r = new Button("button1");
Button r2 = new Button("button1");
Button r3 = new Button("button1");
f.getChildren().addAll(r,r2,r3);
root.getChildren().add(f);
Scene scene = new Scene(root,600,600);
keyEventHandler = new EventHandler<KeyEvent>() {
public void handle(final KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.LEFT || keyEvent.getCode() == KeyCode.UP) {
System.out.println("arrow keys");
}else{
System.out.println(keyEvent);
}
}
};
stage.addEventHandler(KeyEvent.KEY_PRESSED, keyEventHandler);
stage.setScene(scene);
stage.show();
}
public static void main(String args[]){
Application.launch(args);
}
}
Any help would be appreciated
Thanks
I had a similar Problem and found an easy workaround. Since the controls navigating Part triggers after the self-implemented EventHandler you can simply stop any further propagation of the KeyEvent by calling
keyevent.consume()
at the end of your handle() method.
First, the answer on this question is similar to answer on this question : JavaFX: How to change the focus traversal policy?
You can change traversal policy, so that traverse will not be done, for some situations.
The other decision - is to add listener of focused property, and drop focus back, when it is not wished (but it will work for all navigation keys):
node.focusedProperty().addListener(new ChangeListener<Boolean>(){
public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
Platform.runLater(new Runnable(){
public void run() {
node.requestFocus();
}
});
}
});
You should use runLater, because of such issue : JavaFx: After dialog, two textfields gains focus instead one
The way how you do this - handler on key press may not work always, because actions may happen on key release too.
In common case, behavior on key presses is determined in behavior class, which is accessible by skin class. If you want to change behavior on keys pressing, you can change behavior class.

Re-firing a consumed event in JavaFX

I am developing a system that allows the user to scan barcodes. The barcode scanner effectively behaves like a keyboard, "typing" each digit of the barcode at super-human speeds. For the sake of this example, let's say that most amount of time between successive "key strokes" is 10 milliseconds.
I began by implementing an EventHandler that listens for numeric KeyEvents on the application's Window. When a KeyEvent arrives, the handler does not yet know if it was entered by a human or by a barcode scanner (it will know 10 milliseconds from now). Unfortunately, I must make a decision now or risk locking up JavaFX's main thread, so I automatically call keyEvent.consume() to prevent it from being handled.
After 10 milliseconds have elapsed, a timer wakes up and decides whether or not the KeyEvent was part of a barcode. If it was, the KeyEvents are concatenated together and handled by the barcode processing logic. Otherwise, I want to let the application handle the KeyEvent normally.
How can I force the application to handle a KeyEvent after I have already called keyEvent.consume() on it?
Here is my take on how this might be done.
The solution works by filtering the key events for the app, cloning them and placing the cloned events in a queue, then consuming the original events in the filter. The cloned event queue is processed at a later time. Events from the barcode reader are not refired. Events that are not from the barcode reader are refired so that the system can process them. Data structures keep track of whether the events have been processed already or not, so that the system can know in the event filter whether it truly has to intercept and consume the events or let them pass through to the standard JavaFX event handlers.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
// delays event key press handling so that some events can be intercepted
// and routed to a bar code a reader and others can be processed by the app.
public class EventRefire extends Application {
public static void main(String[] args) { launch(args); }
#Override public void start(final Stage stage) throws Exception {
// create the scene.
final VBox layout = new VBox();
final Scene scene = new Scene(layout);
// create a queue to hold delayed events which have not yet been processed.
final List<KeyEvent> unprocessedEventQueue = new ArrayList();
// create a queue to hold delayed events which have already been processed.
final List<KeyEvent> processedEventQueue = new ArrayList();
// create some controls for the app.
final TextField splitterField1 = new TextField(); splitterField1.setId("f1");
final TextField splitterField2 = new TextField(); splitterField2.setId("f2");
final Label forBarCode = new Label();
final Label forTextField = new Label();
// filter key events on the textfield and don't process them straight away.
stage.addEventFilter(KeyEvent.ANY, new EventHandler<KeyEvent>() {
#Override public void handle(KeyEvent event) {
if (event.getTarget() instanceof Node) {
if (!processedEventQueue.contains(event)) {
unprocessedEventQueue.add((KeyEvent) event.clone());
event.consume();
} else {
processedEventQueue.remove(event);
}
}
}
});
// set up a timeline to simulate handling delayed event processing from
// the barcode scanner.
Timeline timeline = new Timeline(
new KeyFrame(
Duration.seconds(1),
new EventHandler() {
#Override public void handle(Event timeEvent) {
// process the unprocessed events, routing them to the barcode reader
// or scheduling the for refiring as approriate.
final Iterator<KeyEvent> uei = unprocessedEventQueue.iterator();
final List<KeyEvent> refireEvents = new ArrayList();
while (uei.hasNext()) {
KeyEvent event = uei.next();
String keychar = event.getCharacter();
if ("barcode".contains(keychar)) {
forBarCode.setText(forBarCode.getText() + keychar);
} else {
forTextField.setText(forTextField.getText() + keychar);
refireEvents.add(event);
}
}
// all events have now been processed - clear the unprocessed event queue.
unprocessedEventQueue.clear();
// refire all of the events scheduled to refire.
final Iterator<KeyEvent> rei = refireEvents.iterator();
while (rei.hasNext()) {
KeyEvent event = rei.next();
processedEventQueue.add(event);
if (event.getTarget() instanceof Node) {
((Node) event.getTarget()).fireEvent(event);
}
}
}
}
)
);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
// layout the scene.
final GridPane grid = new GridPane();
grid.addRow(0, new Label("Input Field 1:"), splitterField1);
grid.addRow(1, new Label("Input Field 2:"), splitterField2);
grid.addRow(2, new Label("For App:"), forTextField);
grid.addRow(3, new Label("For BarCode:"), forBarCode);
grid.setStyle("-fx-padding: 10; -fx-vgap: 10; -fx-hgap: 10; -fx-background-color: cornsilk;");
Label instructions = new Label("Type letters - key events which generate the lowercase letters b, a, r, c, o, d, e will be routed to the barcode input processor, other key events will be routed back to the app and processed normally.");
instructions.setWrapText(true);
layout.getChildren().addAll(grid, instructions);
layout.setStyle("-fx-padding: 10; -fx-vgap: 10; -fx-background-color: cornsilk;");
layout.setPrefWidth(300);
stage.setScene(scene);
stage.show();
}
}
Sample program output:
Because I use a Timeline everything in my code runs on the FXApplicationThread, so I don't have to worry about concurrency in my implementation. In implementation with a real barcode reader and barcode event processor, you may need some added concurrency protection as possibly multiple threads will be involved. Also you might not need the Timeline used in my code to simulate the delayed processing of the barcode system.

Categories