Search code examples
javaevent-handlingjavafx-2barcode-scanner

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?


Solution

  • 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:

    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.