Wednesday, November 20, 2019

Basic JavaFX Projects (Part 4)

JavaFX Keyboard Event Propagation

This is an examination of keyboard event propagation.  As in a previous example, the code for this has EventFilters and EventHandlers registered for various user interface objects which simply print to the console when they have been called.  This allows the motion of the event through the various filters and handlers to be observed.

The GitHub link for the code is: https://github.com/cajanssen/JavaFXKeyEventPropagation.git
If additional help is needed to pull the code into Eclipse and set up a JavaFX project, some information to this effect is available in a previous post.

To reiterate what was said before: The user interface is a nested tree, user interface events will occur within the visual bounding area of more than one object (i.e. the topmost object, its parent, its grandparent, etc.).  These events will propagate through the tree (scene graph) such that all the relevant objects have a chance at reacting to them.   The user interface objects can have EventHandler objects attached to them in two ways: via Event filters and via Event handlers.  The difference between these two is when they execute.  Events propagate down the tree from the top in what is called the capturing phase.  Event filters are called during this time.  When the bottom of the tree is reached, the Event propagates back up the tree in what is called the bubbling phase.  Event handlers are called during this time.  Event propagation can be halted at any time by calling consume() on the actual Event in the handler code.

Javadocs for the JavaFX classes can be found here: https://openjfx.io/javadoc/13/allclasses-index.html

Example code:

package jansproj.basicfx;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class KeyEventPropagationApp extends Application
{
    public KeyEventPropagationApp()
    {
        // TODO Auto-generated constructor stub
    }

    @Override
    public void start(Stage stage) throws Exception
    {
        stage.setTitle("Key Event Propagation");
        VBox root = new VBox();
        HBox colBox = new HBox();
        VBox cola = new VBox();
        Label colaLaba = new Label("Column A");
        // uncomment this to enable focus on this Label
        //colaLaba.setFocusTraversable(true);
        Button colaButta = new Button("Column A");
        TextArea colaText = new TextArea();
        VBox colb = new VBox();
        Label colbLaba = new Label("Column B");
        Button colbButta = new Button("Column B");
        TextArea colbText = new TextArea();

        Scene scene = new Scene(root);
        MenuBar menubar = new MenuBar();
        MenuItem mia = new MenuItem("Item A");
        MenuItem mib = new MenuItem("Item B");
        MenuItem mic = new MenuItem("Item C");
        MenuItem mid = new MenuItem("Item D");
        MenuItem mie = new MenuItem("Item E");
        MenuItem mif = new MenuItem("Item F");
        MenuItem mig = new MenuItem("Item G");
        MenuItem mih = new MenuItem("Item H");
        MenuItem mii = new MenuItem("Item I");
        Menu ma = new Menu("Menu A");
        Menu mb = new Menu("Menu B");
        Menu mc = new Menu("Menu C");
        ma.getItems().addAll(mia, mib, mic);
        mb.getItems().addAll(mid, mie, mif);
        mc.getItems().addAll(mig, mih, mii);
        menubar.getMenus().addAll(ma, mb, mc);

        root.getChildren().add(menubar);
        root.getChildren().add(colBox);
        colBox.getChildren().add(cola);
        cola.getChildren().add(colaLaba);
        cola.getChildren().add(colaButta);
        cola.getChildren().add(colaText);
        colBox.getChildren().add(colb);
        colb.getChildren().add(colbLaba);
        colb.getChildren().add(colbButta);
        colb.getChildren().add(colbText);

        scene.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("Scene KeyPressed handler keycode= " + keyCode);
                }});
        scene.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("Scene KeyPressed filter keycode= " + keyCode);
                }});
        root.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("root (VBox) KeyPressed handler keycode= " + keyCode);
                }});
        root.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("root (VBox) KeyPressed filter keycode= " + keyCode);
                }});
        menubar.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("menubar (MenuBar) KeyPressed handler keycode= " + keyCode);
                }});
        menubar.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("menubar (MenuBar) KeyPressed filter keycode= " + keyCode);
                }});
        ma.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("ma (Menu) KeyPressed handler keycode= " + keyCode);
                }});
        mb.addEventHandler(KeyEvent.KEY_PRESSED,
                new EventHandler<KeyEvent>() {
                    public void handle(KeyEvent e) {
                        KeyCode keyCode = e.getCode();
                        System.out.println("mb (Menu) KeyPressed handler keycode= " + keyCode);
                    }});
        mc.addEventHandler(KeyEvent.KEY_PRESSED,
                new EventHandler<KeyEvent>() {
                    public void handle(KeyEvent e) {
                        KeyCode keyCode = e.getCode();
                        System.out.println("mc (Menu) KeyPressed handler keycode= " + keyCode);
                    }});

        cola.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("cola (VBox) KeyPressed handler keycode= " + keyCode);
                }});
        cola.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("cola (VBox) KeyPressed filter keycode= " + keyCode);
                }});
        colaLaba.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaLaba (Label) KeyPressed handler keycode= " + keyCode);
                }});
        colaLaba.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaLaba (Label) KeyPressed filter keycode= " + keyCode);
                }});
        colaButta.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaButta (Button) KeyPressed handler keycode= " + keyCode);
                }});
        colaButta.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaButta (Button) KeyPressed filter keycode= " + keyCode);
                }});
        colaText.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaText (TextArea) KeyPressed handler keycode= " + keyCode);
                }});
        colaText.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colaText (TextArea) KeyPressed filter keycode= " + keyCode);
                }});

        colb.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colb (VBox) KeyPressed handler keycode= " + keyCode);
                }});
        colb.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colb (VBox) KeyPressed filter keycode= " + keyCode);
                }});
        colbLaba.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbLaba (Label) KeyPressed handler keycode= " + keyCode);
                }});
        colbLaba.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbLaba (Label) KeyPressed filter keycode= " + keyCode);
                }});
        colbButta.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbButta (Button) KeyPressed handler keycode= " + keyCode);
                }});
        colbButta.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbButta (Button) KeyPressed filter keycode= " + keyCode);
                }});
        colbText.addEventHandler(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbText (TextArea) KeyPressed handler keycode= " + keyCode);
                }});
        colbText.addEventFilter(KeyEvent.KEY_PRESSED,
            new EventHandler<KeyEvent>() {
                public void handle(KeyEvent e) {
                    KeyCode keyCode = e.getCode();
                    System.out.println("colbText (TextArea) KeyPressed filter keycode= " + keyCode);
                }});

        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args)
    {
        launch(args);
    }
}

Note that due to the redundancy in this code, it is compressed and formatted less explicitly than might be expected.

So routing of keyboard events is less straightforward than mouse events.  With a mouse event, it is usually pretty clear where it happened and, by extension, which user interface elements are involved by virtue of their location.  A keystroke is not necessarily tied to particular location in the window that is displayed.  Which UI elements get notified of the event will change depending upon what elements are in the window, which element has focus and also what type of keystroke occurred.

Upon executing the application, it can be seen that the initial focus is on the Column A Button.  Pressing a regular letter key shows the event passing through the order of scene filter, root filter, cola filter, cola button filter, cola button handler, cola handler, root handler and scene handler.  (Although, if the object hierarchy creation is examined, it would be noticed that colbox would also be in the chain but no handlers or filters have been added for it.)

If the Return key is pressed, the focus is still on the Column A Button and the button apparently consumes the key event at its handler, since the event makes it down the filter chain and ends at the Column A Button handler.

Clicking on other objects such as the Column B Button or the TextAreas causes the key events to route to these objects instead.  It can be noticed that the TextArea will consume the Arrow key events as well as the Return key events.

The arrow keys can be used to change object focus as well, as long as the current focused object does not consume the event.

Also note Labels don't receive navigation focus by default.  This is because their focusTraversable property is set to "false" by default.  This can be changed by calling the setFocusTraversable() method on the individual Label objects (inherited from the Node object).  Labels still won't receive focus by being clicked upon, however.

Finally, the MenuBar.  While you can add Filters and Handlers, they don't seem to be called.  However, pressing ALT apparently wakes up the MenuBar which then consumes Arrow key events and ignores other key events until Return, which puts it away again.

Because of the many permutations of which object has focus and which key is causing the event, this code is just a jumping off point for exploring keyboard event routing.

Friday, November 1, 2019

Basic JavaFX projects (Part 3)

JavaFX Mouse Event Registration And Propagation

This is an examination of registering InputEvents, in this case MouseEvents (clicked), Event propagation, and how the registered methods are called in response to the Event propagation.

The code can be cloned from github.com as before.  The link is https://github.com/cajanssen/JavaFXMouseEventRegistration.git  Refer to earlier postings for advice on how to bring a GitHub project into Eclipse.

The JavaFX user interface, like many other user interface frameworks, is built upon a nested tree of user interface objects.  (JavaFX uses the term "scene graph".)  In the JavaFX case, it is the scene graph is a collection of Node objects.  Note that the "root" Node ("root" being the top of the content tree) actually needs to be a Parent, which is a direct subclass of Node.  (Stage -> Scene -> Parent -> many Node(s))  The Stage is the equivalent of a window, and if more than one window is desired for an application, additional Stage objects must be created.

https://openjfx.io/javadoc/13/javafx.graphics/javafx/stage/Stage.html
https://openjfx.io/javadoc/13/javafx.graphics/javafx/scene/Scene.html
https://openjfx.io/javadoc/13/javafx.graphics/javafx/scene/Node.html
https://openjfx.io/javadoc/13/javafx.graphics/javafx/scene/Parent.html

Note that, as evidenced by the URL, the documentation links point to a specific version of JavaFX (i.e., 13).  So, depending upon how Gluon maintains the documentation over time, these links may eventually break.  Currently, links back through version 11 are active.

Since this user interface is a nested tree, user interface events will occur within the visual bounding area of more than one object (i.e. the topmost object, its parent, its grandparent, etc.).  These events will propagate through the tree (scene graph) such that all the relevant objects have a chance at reacting to them.   The user interface objects can have EventHandler objects attached to them in two ways: via Event filters and via Event handlers.  The difference between these two is when they execute.  Events propagate down the tree from the top in what is called the capturing phase.  Event filters are called during this time.  When the bottom of the tree is reached, the Event propagates back up the tree in what is called the bubbling phase.  Event handlers are called during this time.  Event propagation can be halted at any time by calling consume() on the actual Event in the handler code.  (In the code below, e.consume() in the handle() method of one of the EventHandler objects.)

Example code:

package jansproj.basicfx;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class MouseEventRegistration extends Application
{
    public MouseEventRegistration()
    {
    }

    // this application waits for and reacts to mouse click events
   
    @Override
    public void start(Stage stage) throws Exception
    {
        // Stage, Scene, root Node - standard stuff common to JavaFX programs
        stage.setTitle("Mouse Event Example");
        Group root = new Group();
        Scene scene = new Scene(root);
        stage.setScene(scene);

        int canvasWidth = 1000;
        int canvasHeight = 500;
        int canvasXCenter = canvasWidth / 2;
        int canvasYCenter = canvasHeight / 2;

        Canvas canvas = new Canvas(canvasWidth, canvasHeight);
       
        // rather than create a large tree of nodes, create a tree of only
        // one node - canvas - and draw on that
        Group extraGroup = new Group();
        extraGroup.getChildren().add(canvas);
        root.getChildren().add(extraGroup);

       
        double blackCircleRadius = 250;
        double blueCircleRadius = 200;
        double redCircleRadius = 150;
        double goldCircleRadius = 100;

        // make concentric circles
        // drawing point for strokeOval() is the upper left corner of bounding box, not the center
        GraphicsContext gc = canvas.getGraphicsContext2D();
        gc.setLineWidth(4.0);
        gc.setStroke(Color.BLACK);
        gc.strokeOval((canvasXCenter - blackCircleRadius), (canvasYCenter - blackCircleRadius), blackCircleRadius*2, blackCircleRadius*2);
         gc.setStroke(Color.BLUE);
        gc.strokeOval((canvasXCenter - blueCircleRadius), (canvasYCenter - blueCircleRadius), blueCircleRadius*2, blueCircleRadius*2);
        gc.setStroke(Color.RED);
        gc.strokeOval((canvasXCenter - redCircleRadius), (canvasYCenter - redCircleRadius), redCircleRadius*2, redCircleRadius*2);
        gc.setStroke(Color.GOLD);
        gc.strokeOval((canvasXCenter - goldCircleRadius), (canvasYCenter - goldCircleRadius), goldCircleRadius*2, goldCircleRadius*2);

       
        // attach a Mouse Click event handler to the scene
        // rather than create the object elsewhere and pass it in to the setOnMouseClicked()
        // method, define the event handler right here with
        // an anonymous inner class - common practice
        scene.setOnMouseClicked(
            new EventHandler<MouseEvent>()
            {
                public void handle(MouseEvent e)
                { System.out.println("Scene event handler (via set)"); }
            });
        root.setOnMouseClicked(
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Root (Group) event handler (via set)"); e.consume();}
                });
        extraGroup.setOnMouseClicked(
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("ExtraGroup event handler (via set) #1"); }
                });
        extraGroup.setOnMouseClicked(
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("ExtraGroup event handler (via set) #2"); }
                });
        extraGroup.addEventHandler(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    {  System.out.println("ExtraGroup event handler #1 (via add)"); }
                });
        extraGroup.addEventHandler(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    {  System.out.println("ExtraGroup event handler #2 (via add)"); }
                });
        extraGroup.addEventHandler(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    {  System.out.println("ExtraGroup event handler #3 (via add)"); }
                });
        canvas.setOnMouseClicked(
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Canvas event handler (via set)"); }
                });
        scene.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Scene event filter #1"); };
                });
        scene.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Scene event filter #2"); }
                });
        scene.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Scene event filter #3"); };
                });
        root.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Root (Group) event filter"); }
                });
        extraGroup.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("ExtraGroup event filter"); }
                });
        canvas.addEventFilter(MouseEvent.MOUSE_CLICKED,
                new EventHandler<MouseEvent>()
                {
                    public void handle(MouseEvent e)
                    { System.out.println("Canvas event filter"); }
                });

        // calling show() on the Stage is a standard requirement for a JavaFX program
        stage.show();
    }

    public static void main(String[] args)
    {
        launch(args);
    }
}


Note that due to the redundancy in this code, it is compressed and formatted less explicitly than might be expected.
Also note that the second Group object, named "extraGroup" in the code, is gratuitous and is only in the code to add depth to the user interface object tree to better display the propagation of events.
During the execution of the application, the mouse clicks will cause each EventHandler object to print to the console an identification of itself, providing a listing of the order in which they executed.

Event filters (EventHandler objects) to be executed during the capture phase are added via the addEventFilter() method.  Note that multiple event filters can be added to each Node type object.  Also, it would appear filters execute in order added.  However, it is not specified as such in the documentation.  Therefore, it would be prudent not to assume that to be guaranteed behavior.  Even if an examination of the source for the framework libraries did show this to be true, if it is not specified in the interface contract (and written in the documentation somewhere) it could change at any time in the future as library components are updated.


Event handlers (again, EventHandler objects) to be executed during the bubbling phase can be added in two ways - with the setOnMouseClicked method (name of the method depends upon event being handled) and the addEventHandler method (type of the event passed as a parameter).  As can be see in examining and executing the example code, only one handler can be added with setOnMouseClicked().  Any subsequent calls on the same Node (or descendant) object overwrites the first handler.  Multiple handlers can be added with addEventHandler().  Also as is seen in the example code, calls to setOnMouseClicked() and addEventHandler() do not interfere with each other.

Finally, note that during execution two of the created handler objects never get called.  The first, "ExtraGroup event handler #1", because another setOnMouseClicked called overwrote the first handler.  (As mentioned above.)  For the second, "Scene event handler", e.consume() is called in the root Event handler and stops propagation before it makes it back up to the top.  The handler "Scene event handler" would have been called at the very end.