1

I've upgraded my app from JavaFX8 to JavaFX11 (OpenJDK with OpenJFX) and am trying to resolve an issue wrt using the keyboard to navigate through TableCells in a TableView.

In JavaFX8, I used setOnKeyPressed to trap and process keys special keys eg. TAB to move to the next right cell, SHIFT+TAB to move to the previous left cell. That worked fine.

However, when the same code runs in JavaFX11, TAB moves focus from the TableView to other controls on the screen and SHIFT+TAB does the same but in reverse control order. It's as if JavaFX11 is reverting to default behaviour for TAB and SHIFT+TAB and ignoring the setOnKeyPressed.

Why does that happen? Has something changed wrt keyboard navigation between JavaFX8 and JavaFX11? Am I doing something wrong in my code?

I modified the JavaFX11 code to register and use an EventHandler rather than setOnKeyPressed. Keyboard navigation then worked like it did in JavaFX8. However, the new EventHandler is interferring with other EventHandlers in my app and I would really like to be able to use setOnKeyPressed.

I've been Googling but have thus far found only one reference, at least part of which looks like the same issue: JavaFX 11 TableView Cell navigation by TAB key pressed without custom editableCell class. Other references were for JavaFX8 or earlier.

Here is an MVCE that demonstrates the issue. The same code runs in both JavaFX8 and JavaFX11. You can run it with no event handling to see default behaviour, or use either setOnKeyPressed or the EventHandler to see the difference in behaviour when TAB and SHIFT+TAB are used.

I'm using jdk1.8.0_202 and the 11.0.2 versions of OpenJDK and OpenJFX, all running in Netbeans 10.0 on Windows 7.

package test009;

import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;

public class Test009 extends Application {

    private EventHandler<KeyEvent> keyEditingHandler;
    private ObservableList<DataModel> ol = FXCollections.observableArrayList();
    private TableView<DataModel> tv = new TableView();

    private Parent createContent() {

        loadDummyData();
        createTableColumns();
        tv.getSelectionModel().setCellSelectionEnabled(true);
        tv.setEditable(true);
        tv.setItems(ol);

        //******************************************************************************************
        //JavaFX8 code that doesn't appear to work in JavaFX11
/*
        tv.setOnKeyPressed(event -> {
            doTheKeyEvent(event);
        });
*/
        //******************************************************************************************
        //Code needed to achieve the same end in JavaFX11
/*
        registerKeyEventHandler();
        addKeyEditingHandler();
*/
        BorderPane content = new BorderPane();
        content.setCenter(tv);

        HBox hb = new HBox();
        hb.setPadding(new Insets(10D));
        hb.setSpacing(10D);
        hb.setAlignment(Pos.CENTER);
        Button btn1 = new Button("any old object");
        Button btn2 = new Button("another object");
        hb.getChildren().addAll(Arrays.asList(btn1, btn2));

        content.setTop(hb);

        return content;
    }

    public void registerKeyEventHandler() {

        keyEditingHandler = (KeyEvent event) -> {
            doTheKeyEvent(event);
        };

    }    

    private <S> void doTheKeyEvent(KeyEvent event) {

        final KeyCombination shiftTAB = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHIFT_DOWN);
        @SuppressWarnings("unchecked") TablePosition<DataModel, ?> pos = tv.getFocusModel().getFocusedCell();

        if ( shiftTAB.match(event) ) {

            tv.getSelectionModel().selectLeftCell();
            event.consume();

        } else if ( event.getCode() == KeyCode.TAB ) {

            tv.getSelectionModel().selectRightCell();
            event.consume();

        //... test for other keys and key combinations
        //... otherwise fall through to edit the TableCell

        } else if ( ! event.isControlDown() && ! event.isAltDown() ){

            tv.edit(pos.getRow(), tv.getVisibleLeafColumn(pos.getColumn()));

        }

    }

    public void addKeyEditingHandler() {

        tv.addEventFilter(KeyEvent.KEY_PRESSED, keyEditingHandler);

    }

    private void createTableColumns() {

        TableColumn<DataModel,String> col1 = new TableColumn<>("field1");
        TableColumn<DataModel,String> col2 = new TableColumn<>("field2");
        TableColumn<DataModel,String> col3 = new TableColumn<>("field3");
        TableColumn<DataModel,String> col4 = new TableColumn<>("field4");
        TableColumn<DataModel,String> col5 = new TableColumn<>("field5");

        col1.setCellValueFactory(cellData -> cellData.getValue().field1Property());
        col1.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        col2.setCellValueFactory(cellData -> cellData.getValue().field2Property());
        col2.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        col3.setCellValueFactory(cellData -> cellData.getValue().field3Property());
        col3.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        col4.setCellValueFactory(cellData -> cellData.getValue().field4Property());
        col4.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        col5.setCellValueFactory(cellData -> cellData.getValue().field5Property());
        col5.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        tv.getColumns().addAll(Arrays.asList(col1, col2, col3, col4, col5));

    }

    private void loadDummyData() {

        ol.add(new DataModel("1", "a", "x", "y", "z"));
        ol.add(new DataModel("2", "a", "x", "y", "z"));
        ol.add(new DataModel("3", "a", "x", "y", "z"));
        ol.add(new DataModel("4", "a", "x", "y", "z"));
        ol.add(new DataModel("5", "a", "x", "y", "z"));

    }

    private class DataModel {

        private final StringProperty field1;
        private final StringProperty field2;
        private final StringProperty field3;
        private final StringProperty field4;
        private final StringProperty field5;

        public DataModel(
            String field1,
            String field2,
            String field3,
            String field4,
            String field5
        ) {
            this.field1 = new SimpleStringProperty(field1);
            this.field2 = new SimpleStringProperty(field2);
            this.field3 = new SimpleStringProperty(field3);
            this.field4 = new SimpleStringProperty(field4);
            this.field5 = new SimpleStringProperty(field5);
        }

        public String getField1() {return field1.get().trim();}
        public void setField1(String field1) {this.field1.set(field1);}
        public StringProperty field1Property() {return field1;}

        public String getField2() {return field2.get().trim();}
        public void setField2(String field2) {this.field2.set(field2);}
        public StringProperty field2Property() {return field2;}

        public String getField3() {return field3.get().trim();}
        public void setField3(String field3) {this.field3.set(field3);}
        public StringProperty field3Property() {return field3;}

        public String getField4() {return field4.get().trim();}
        public void setField4(String field4) {this.field4.set(field4);}
        public StringProperty field4Property() {return field4;}

        public String getField5() {return field5.get().trim();}
        public void setField5(String field5) {this.field5.set(field5);}
        public StringProperty field5Property() {return field5;}

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("JavaFX11 - TableView keyboard navigation");
        stage.setWidth(600D);
        stage.setHeight(600D);
        stage.show();
    }

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

}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
GreenZebra
  • 362
  • 6
  • 14
  • What handlers are being interfered with when you use `addEventHandler` but aren't interfered with when you use `setOnKeyPressed`? – Slaw Apr 06 '19 at 04:38
  • I have a key `EventHandler` for custom `TableCells` & a mouse `EventHandler` on the `TableView`. The custom cells have data validation & I don't allow the user to TAB or click out of a cell if there's a data error. So, the `setOnKeyPressed` handles navigation when cells aren't being edited and the key & mouse `EventHandlers` manage navigation during editing. When I changed the `setOnKeyPressed` to an `EventHandler` in JFX11, the custom cell handler never seemed to be invoked for keys that were also trapped in the new handler eg. TAB. It makes sense but I'm not sure how to get around it. – GreenZebra Apr 06 '19 at 05:33
  • 2
    Well, the internal behavior classes went through some changes between FX 8 and FX 9; in part, I believe, due to the skin classes becoming public. Can't speak to FX 8, but in FX 12 the internal `InputMap` class doesn't handle the event if it's consumed—but that makes it dependent on the order event handlers are called. Since `onXXX` handlers are invoked _after_ `addEventHandler` handlers, using `onKeyPressed` won't work because you consume the event after it's been processed by the `InputMap`. When you use `addEventHandler`, you're adding the handler _before_ (cont.) – Slaw Apr 06 '19 at 05:41
  • 2
    ... the `InputMap` is added to the node via the same method. Thus, your handler is invoked first, consumes the event, and the `InputMap` doesn't process the event. But the `InputMap` is added when the table view's skin is created; this normally occurs once the window is shown. If you were to add your handler _after_ the window was shown you'd run into the same problems as when using `onKeyPressed`. However, this is all just an explanation on why certain approaches seem to work. How to solve your question, I'm not sure (at least not yet). – Slaw Apr 06 '19 at 05:43
  • Thanks, Slaw, that's very helpful and explains the handler "interference". But I have some research to do as I've not come across `InputMap`s before and don't understand their relationship with `EventHandler`s or the `setOnXXX` methods. I shall do some research and then have another go at trying to resolve my navigation issue. Fun! :-) – GreenZebra Apr 06 '19 at 06:20
  • 1
    Just remember that `InputMap`, as well as the behavior classes, are internal API; they can change without notice. Also, regarding `onXXX` handlers being invoked after `addEventHandler` handlers, I'm not sure if that's an implementation detail or if it's documented anywhere. Basically, relying on the these details may cause your code to break in a future release. And regarding your cells not getting key events, that might have to do with focus—only focused nodes get key input events. (_Note: the `FocusModel` is not the same thing as `Node.focused`_). – Slaw Apr 06 '19 at 06:31
  • Will do re the cautions about internal APIs. It's more me wanting to understand how things work. And thanks for the tip re focus. I hadn't thought about that. Will have another look at my code in the morning. Cheers. – GreenZebra Apr 06 '19 at 07:00
  • @Slaw the sequence (handler registered with setOnXX being invoked after those registered with addXX) definitely is specified ... but can't find it right now ;) – kleopatra Apr 15 '19 at 11:11
  • 2
    @Slaw it's described in the basic tutorial for event processing https://docs.oracle.com/javafx/2/events/processing.htm#CEGJAAFD - not sure if that counts as specification, though – kleopatra Apr 15 '19 at 11:40
  • 1
    @kleopatra I _knew_ I read about this behavior somewhere, but not sure if a tutorial counts as specification either (I can't find any mention in the Javadoc). – Slaw Apr 15 '19 at 11:46
  • @Slaw we could see it as a detailed spell-out of the rather terse spec of setOnXX (not all but f.i. for XX == keyPressed in node): _The function is called only if the event hasn't been already consumed during its capturing or bubbling phase._ which implies being last .. somehow .. – kleopatra Apr 15 '19 at 12:01
  • @kleopatra Hmm... except that the implementation doesn't follow that documentation. The onKeyPressed handler _is_ invoked (with the consumed event) even when consumed in an addEventHandler handler. This is what I've always expected to happen, though. – Slaw Apr 15 '19 at 12:10
  • @Slaw oops .. you are right, never noticed .. doooh who writes the bug report ;) – kleopatra Apr 15 '19 at 12:35
  • @kleopatra For this one, I think I'll let you do the honors ;) – Slaw Apr 15 '19 at 12:48
  • @Slaw *hehe ... meanwhile, I think that it's only the wording of the api doc which is not clear enough, the impl does the right thing: as the order on the same level (which seems to imply when registering multiple handlers for the same type of event on the same instance) is explicitly unspecified (except for the convenience handler notified last), all those must receive the event, even if any previously called consumed it - otherwise we would get completely unpredictable behaviour. Hmmm .. – kleopatra Apr 15 '19 at 13:23

0 Answers0