javajavafxtableviewfocuscontrolsfx

How do I get keyboard navigation for the SelectedRow in controlsfx.TableView2?


I'm using the TableView2 component from the library ControlsFX. In an simple example (see sourcecode below) the ArrowKey-Navigation in the table is gone after changing import javafx.scene.control.TableView to import org.controlsfx.control.tableview2.TableView2.

Done 'Research'

I read in the JavaDocs that TableView2 is a drop-in-replacement and so I'm asking what I can do to bring back the functionality of the core-component.

Description of the problem

The example code is from the Oracle-Tutorials, I just deleted some unnecessary stuff. Try mouseclicking a table cell and press the arrow-down key. The TableSelection is not moving one row down, but the whole Focus is traversed to the TextField.

2

I'm using Windows 10 Pro 22H2, JDK corretto-17.0.7, JavaFX 20.0.1 and controlsfx-11.1.2

Example Code

If you change new TableView2() to new TableView() everything works as expected.

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.tableview2.TableView2;

/**
 * Reduced from https://docs.oracle.com/javafx/2/ui_controls/table-view.htm.
 */
public class TableView2KeyboardNavigation extends Application
{
  static class Main
  {
    public static void main( String[] args )
    {
      Application.launch( TableView2KeyboardNavigation.class, args );
    }
  }

  
  private       TableView<Person>      table = new TableView2<>();
  private final ObservableList<Person> data  =
      FXCollections.observableArrayList(
          new Person( "Jacob", "Smith" ),
          new Person( "Isabella", "Johnson" ),
          new Person( "Ethan", "Williams" )
      );


  @Override
  public void start( Stage stage )
  {
    final var scene = new Scene( new Group() );
    stage.setTitle( "TableView2 Sample" );

    final var firstNameCol = new TableColumn( "First Name" );
    firstNameCol.setCellValueFactory(
        new PropertyValueFactory<Person, String>( "firstName" ) );
    final var lastNameCol = new TableColumn( "Last Name" );
    lastNameCol.setCellValueFactory(
        new PropertyValueFactory<Person, String>( "lastName" ) );

    table.setItems( data );
    table.getColumns().addAll( firstNameCol, lastNameCol );

    final var vbox = new VBox();
    vbox.getChildren().addAll( table, new TextField( "Focus lands here after ArrowDown-Key..." ) );

    ( (Group) scene.getRoot() ).getChildren().addAll( vbox );
    stage.setScene( scene );
    stage.show();
  }
  
  
  public static class Person
  {
    private final SimpleStringProperty firstName;
    private final SimpleStringProperty lastName;

    private Person( String fName, String lName )
    {
      this.firstName = new SimpleStringProperty( fName );
      this.lastName = new SimpleStringProperty( lName );
    }

    public String getFirstName()
    {
      return firstName.get();
    }
    public void setFirstName( String fName )
    {
      firstName.set( fName );
    }

    public String getLastName()
    {
      return lastName.get();
    }
    public void setLastName( String fName )
    {
      lastName.set( fName );
    }
  }

}

Solution

  • What you have observed is correct. It is indeed, that the default behaviour that is available in TableView is supressed or removed in TableView2.

    The general key mapping behavior of TableView is implemented in TableViewBehaviorBase.java as below:

    addDefaultMapping(tableViewInputMap,
                    new KeyMapping(TAB, FocusTraversalInputMap::traverseNext),
                    new KeyMapping(new KeyBinding(TAB).shift(), FocusTraversalInputMap::traversePrevious),
    
                    new KeyMapping(HOME, e -> selectFirstRow()),
                    new KeyMapping(END, e -> selectLastRow()),
    
                    new KeyMapping(PAGE_UP, e -> scrollUp()),
                    new KeyMapping(PAGE_DOWN, e -> scrollDown()),
    
                    new KeyMapping(LEFT, e -> { if(isRTL()) selectRightCell(); else selectLeftCell(); }),
                    new KeyMapping(KP_LEFT,e -> { if(isRTL()) selectRightCell(); else selectLeftCell(); }),
                    new KeyMapping(RIGHT, e -> { if(isRTL()) selectLeftCell(); else selectRightCell(); }),
                    new KeyMapping(KP_RIGHT, e -> { if(isRTL()) selectLeftCell(); else selectRightCell(); }),
    
                    new KeyMapping(UP, e -> selectPreviousRow()),
                    new KeyMapping(KP_UP, e -> selectPreviousRow()),
                    new KeyMapping(DOWN, e -> selectNextRow()),
                    new KeyMapping(KP_DOWN, e -> selectNextRow()),
    
                    new KeyMapping(LEFT,   e -> { if(isRTL()) focusTraverseRight(); else focusTraverseLeft(); }),
                    new KeyMapping(KP_LEFT, e -> { if(isRTL()) focusTraverseRight(); else focusTraverseLeft(); }),
                    new KeyMapping(RIGHT, e -> { if(isRTL()) focusTraverseLeft(); else focusTraverseRight(); }),
                    new KeyMapping(KP_RIGHT, e -> { if(isRTL()) focusTraverseLeft(); else focusTraverseRight(); }),
                    new KeyMapping(UP, FocusTraversalInputMap::traverseUp),
                    new KeyMapping(KP_UP, FocusTraversalInputMap::traverseUp),
                    new KeyMapping(DOWN, FocusTraversalInputMap::traverseDown),
                    new KeyMapping(KP_DOWN, FocusTraversalInputMap::traverseDown),
    
                    new KeyMapping(new KeyBinding(HOME).shift(), e -> selectAllToFirstRow()),
                    new KeyMapping(new KeyBinding(END).shift(), e -> selectAllToLastRow()),
                    new KeyMapping(new KeyBinding(PAGE_UP).shift(), e -> selectAllPageUp()),
                    new KeyMapping(new KeyBinding(PAGE_DOWN).shift(), e -> selectAllPageDown()),
    
                    new KeyMapping(new KeyBinding(UP).shift(), e -> alsoSelectPrevious()),
                    new KeyMapping(new KeyBinding(KP_UP).shift(), e -> alsoSelectPrevious()),
                    new KeyMapping(new KeyBinding(DOWN).shift(), e -> alsoSelectNext()),
                    new KeyMapping(new KeyBinding(KP_DOWN).shift(), e -> alsoSelectNext()),
    
                    new KeyMapping(new KeyBinding(SPACE).shift(), e -> selectAllToFocus(false)),
                    new KeyMapping(new KeyBinding(SPACE).shortcut().shift(), e -> selectAllToFocus(true)),
    
                    new KeyMapping(new KeyBinding(LEFT).shift(), e -> { if(isRTL()) alsoSelectRightCell(); else alsoSelectLeftCell(); }),
                    new KeyMapping(new KeyBinding(KP_LEFT).shift(),  e -> { if(isRTL()) alsoSelectRightCell(); else alsoSelectLeftCell(); }),
                    new KeyMapping(new KeyBinding(RIGHT).shift(),  e -> { if(isRTL()) alsoSelectLeftCell(); else alsoSelectRightCell(); }),
                    new KeyMapping(new KeyBinding(KP_RIGHT).shift(), e -> { if(isRTL()) alsoSelectLeftCell(); else alsoSelectRightCell(); }),
    
                    new KeyMapping(new KeyBinding(UP).shortcut(), e -> focusPreviousRow()),
                    new KeyMapping(new KeyBinding(DOWN).shortcut(), e -> focusNextRow()),
                    new KeyMapping(new KeyBinding(RIGHT).shortcut(), e -> { if(isRTL()) focusLeftCell(); else focusRightCell(); }),
                    new KeyMapping(new KeyBinding(KP_RIGHT).shortcut(), e -> { if(isRTL()) focusLeftCell(); else focusRightCell(); }),
                    new KeyMapping(new KeyBinding(LEFT).shortcut(), e -> { if(isRTL()) focusRightCell(); else focusLeftCell(); }),
                    new KeyMapping(new KeyBinding(KP_LEFT).shortcut(), e -> { if(isRTL()) focusRightCell(); else focusLeftCell(); }),
    
                    new KeyMapping(new KeyBinding(A).shortcut(), e -> selectAll()),
                    new KeyMapping(new KeyBinding(HOME).shortcut(), e -> focusFirstRow()),
                    new KeyMapping(new KeyBinding(END).shortcut(), e -> focusLastRow()),
                    new KeyMapping(new KeyBinding(PAGE_UP).shortcut(), e -> focusPageUp()),
                    new KeyMapping(new KeyBinding(PAGE_DOWN).shortcut(), e -> focusPageDown()),
    
                    new KeyMapping(new KeyBinding(UP).shortcut().shift(), e -> discontinuousSelectPreviousRow()),
                    new KeyMapping(new KeyBinding(DOWN).shortcut().shift(), e -> discontinuousSelectNextRow()),
                    new KeyMapping(new KeyBinding(LEFT).shortcut().shift(), e -> { if(isRTL()) discontinuousSelectNextColumn(); else discontinuousSelectPreviousColumn(); }),
                    new KeyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> { if(isRTL()) discontinuousSelectPreviousColumn(); else discontinuousSelectNextColumn(); }),
                    new KeyMapping(new KeyBinding(PAGE_UP).shortcut().shift(), e -> discontinuousSelectPageUp()),
                    new KeyMapping(new KeyBinding(PAGE_DOWN).shortcut().shift(), e -> discontinuousSelectPageDown()),
                    new KeyMapping(new KeyBinding(HOME).shortcut().shift(), e -> discontinuousSelectAllToFirstRow()),
                    new KeyMapping(new KeyBinding(END).shortcut().shift(), e -> discontinuousSelectAllToLastRow()),
    
                    enterKeyActivateMapping = new KeyMapping(ENTER, this::activate),
                    new KeyMapping(SPACE, this::activate),
                    new KeyMapping(F2, this::activate),
                    escapeKeyCancelEditMapping = new KeyMapping(ESCAPE, this::cancelEdit),
    
                    new InputMap.MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed)
            );
    

    And this behavior implementation is set to TableView in TableViewSkin class. (Please note that it is not in TableViewSkinBase).

    Now coming to TableView2 implementation, the skin of TableView2 is TableView2Skin which also extends TableViewSkinBase. But there is no behavior defined in this TableView2Skin. This is the reason why you cannot see the same behavior.

    In short, TableView2 is definitely not a complete extension to TableView. Though TableView2 extends TableView, the TableView2Skin is not extending TableViewSkin. So you will not get all the features of TableView. And I am not sure whether this is an intended decision or not :).

    And to get the things work as expected, it is not as easy as just adding a key handler to TableView like tableView.setOnKeyPressed(e->...select next row...);. This will work for basic implementation. But don't expect to be as same as TableView. Because in TableVieBehavior a lot of stuff is handled to do the row selection.

    Below is the code that will be executed, when a key press is done in TableView.

    new KeyMapping(DOWN, e -> selectNextRow()),
    
    protected void selectNextRow() {
            selectCell(1, 0);
            if (onSelectNextRow != null) onSelectNextRow.run();
        }
    
    protected void selectCell(int rowDiff, int columnDiff) {
            TableSelectionModel sm = getSelectionModel();
            if (sm == null) return;
    
            TableFocusModel fm = getFocusModel();
            if (fm == null) return;
    
            TablePositionBase<TC> focusedCell = getFocusedCell();
            int currentRow = focusedCell.getRow();
            int currentColumn = getVisibleLeafIndex(focusedCell.getTableColumn());
    
            if (rowDiff > 0 && currentRow >= getItemCount() - 1) return;
            else if (columnDiff < 0 && currentColumn <= 0) return;
            else if (columnDiff > 0 && currentColumn >= getVisibleLeafColumns().size() - 1) return;
            else if (columnDiff > 0 && currentColumn == -1) return;
    
            TableColumnBase tc = focusedCell.getTableColumn();
            tc = getColumn(tc, columnDiff);
    
            //JDK-8222214: Moved this "if" here because the first row might be focused and not selected, so
            // this makes sure it gets selected when the users presses UP. If not it ends calling
            // VirtualFlow.scrollTo(-1) at and the content of the TableView disappears.
            int row = (currentRow <= 0 && rowDiff <= 0) ? 0 : focusedCell.getRow() + rowDiff;
            sm.clearAndSelect(row, tc);
            setAnchor(row, tc);
        }
    
     protected void setAnchor(int row, TableColumnBase col) {
            setAnchor(row == -1 && col == null ? null : getTablePosition(row, col));
        }
    
    public static <T> void setAnchor(Control control, T anchor, boolean isDefaultAnchor) {
            if (control == null) return;
            if (anchor == null) {
                removeAnchor(control);
            } else {
                control.getProperties().put(ANCHOR_PROPERTY_KEY, anchor);
                control.getProperties().put(IS_DEFAULT_ANCHOR_KEY, isDefaultAnchor);
            }
        }
    

    It is up to you to take the decision of which TableView to use for your needs. If you need TableView2, then you need to somehow include the above code to your TableView2. At a basic level you can include the below one:

    tableView2.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
                int index = tableView2.getSelectionModel().getSelectedIndex();
                if (e.getCode() == KeyCode.DOWN) {
                    if (index < tableView2.getItems().size() - 1) {
                        index++;
                    }
                } else if (e.getCode() == KeyCode.UP) {
                    if (index > 0) {
                        index--;
                    }
                }
                tableView2.getSelectionModel().select(index);
    
                // If I don't consume this event, the focus will move away to next control.
                e.consume();
            });
    

    Below is a quick demo that demonstrates this solution on TableView2. enter image description here

    import javafx.application.Application;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import org.controlsfx.control.tableview2.TableView2;
    
    import java.util.stream.Stream;
    
    public class TableView2Demo extends Application {
        @Override
        public void start(final Stage stage) throws Exception {
            ObservableList<Person> persons = FXCollections.observableArrayList();
            persons.add(new Person("Harry", "John", "LS"));
            persons.add(new Person("Mary", "King", "MS"));
            persons.add(new Person("Don", "Bon", "CAT"));
    
            VBox root = new VBox();
            root.setSpacing(5);
            root.setPadding(new Insets(5));
    
            TableView<Person> tableView1 = new TableView<>();
            tableView1.setId("TableView");
    
            TableView2<Person> tableView2 = new TableView2<>();
            tableView2.setId("TableView2");
    
            Stream.of(tableView1, tableView2).forEach(tableView -> {
                TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
                fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
    
                TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
                lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
    
                TableColumn<Person, String> cityCol = new TableColumn<>("City");
                cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
    
                fnCol.setPrefWidth(150);
                lnCol.setPrefWidth(100);
                cityCol.setPrefWidth(200);
    
                tableView.getColumns().addAll(fnCol, lnCol, cityCol);
                tableView.setItems(persons);
                Label lbl = new Label(tableView.getId());
                lbl.setMinHeight(30);
                lbl.setAlignment(Pos.BOTTOM_LEFT);
                lbl.setStyle("-fx-font-weight:bold;-fx-font-size:16px;");
                root.getChildren().addAll(lbl, tableView);
                VBox.setVgrow(tableView, Priority.ALWAYS);
            });
    
            // You need to add a handler to get focused when mouse clicked.
            tableView2.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> tableView2.requestFocus());
    
            // And another handler for row selection
            tableView2.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
                int index = tableView2.getSelectionModel().getSelectedIndex();
                if (e.getCode() == KeyCode.DOWN) {
                    if (index < tableView2.getItems().size() - 1) {
                        index++;
                    }
                } else if (e.getCode() == KeyCode.UP) {
                    if (index > 0) {
                        index--;
                    }
                }
                tableView2.getSelectionModel().select(index);
    
                // If I don't consume this event, the focus will move away to next control.
                e.consume();
            });
    
            Scene scene = new Scene(root, 500, 400);
            stage.setScene(scene);
            stage.setTitle("TableView2 Demo");
            stage.show();
        }
    
        class Person {
            private StringProperty firstName = new SimpleStringProperty();
            private StringProperty lastName = new SimpleStringProperty();
            private StringProperty city = new SimpleStringProperty();
    
            public Person(String fn, String ln, String cty) {
                setFirstName(fn);
                setLastName(ln);
                setCity(cty);
            }
    
            public String getFirstName() {
                return firstName.get();
            }
    
            public StringProperty firstNameProperty() {
                return firstName;
            }
    
            public void setFirstName(String firstName) {
                this.firstName.set(firstName);
            }
    
            public String getLastName() {
                return lastName.get();
            }
    
            public StringProperty lastNameProperty() {
                return lastName;
            }
    
            public void setLastName(String lastName) {
                this.lastName.set(lastName);
            }
    
            public String getCity() {
                return city.get();
            }
    
            public StringProperty cityProperty() {
                return city;
            }
    
            public void setCity(String city) {
                this.city.set(city);
            }
        }
    }