1

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.

  • The broken navigation is observable in the ControlsFX Sampler application as well.
  • Maybe unrelated: There is also an SpreadsheetView example which is using a different looking CellSelection-style.

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 );
    }
  }

}
bobndrew
  • 395
  • 10
  • 32

2 Answers2

2

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);
        }
    }
}
Sai Dandem
  • 8,229
  • 11
  • 26
  • 1
    I would appreciate the reason for the down vote. Which will help me to understand where my analyzation went wrong :) – Sai Dandem Jul 12 '23 at 00:14
  • If I have to guess, the person was expecting a shorter answer and didn't want to bother reading it :D. In my opinion this is an excellent answer. Also there is already and issue reported for this in the repo: https://github.com/controlsfx/controlsfx/issues/1493. – vl4d1m1r4 Jul 13 '23 at 10:54
0

There's no such thing as a "drop-in replacement" ... it's a fairy tale.

what you did there is create a table view, fill it with some stuff, and plop it in a scene container of a window. nothing else.

to handle specific key listener stuff, you need to use table.setOnKeyPressed(some_event_handler) in order to assign a specific javafx.event.EventHandler which would do what you tell it to.

i advise you to use a javafx.scene.control.TableView instance in debug mode in whatever case you have going on which actually works... figure out where that onKeyPressed/Released thing is being declared (as in ... where the code block that gets executed once the listener kicks in a.k.a. handler) ... and go from there ... then duplicate that block of code more or less after you declare your instance of tableView2

it should look something like

tableView.setOnKeyReleased((KeyEvent t)-> { // or key pressed ... or key down... 
    KeyCode key=t.getCode();
    if (key==KeyCode.DOWN){
        int pos=tableView.getSelectionModel().getSelectedIndex();
        System.out.println("key released while at position :: "+ pos);
    }

    // add similar for UP ... maybe left or right 

});
EverNight
  • 964
  • 7
  • 16