Search code examples
javajavafxtableviewright-to-left

Is there a way for changing TableView focus traversable behavior?


I have a simple TableView with Right-To-Left node orientation and setCellSelectionEnabled(true). Now, when the right arrow key is pressed on the table, focus traversable acts on the opposite side, so the left cell will be selected. This is also true for another side; When the left arrow key is pressed, the right-hand side cell will be selected. For region purposes, the node orientation property of TableView must remain RTL. So how can I solve this issue?

Here is TableView FXML code:

<TableView fx:id="table" editable="true" layoutX="97.0" layoutY="170.0" nodeOrientation="RIGHT_TO_LEFT" prefHeight="400.0" prefWidth="816.0" AnchorPane.bottomAnchor="25.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="121.0">
         <placeholder>
            <Label text="داده ای یافت نشد :(" />
         </placeholder>
         <columns>
            <TableColumn fx:id="numberColumn" maxWidth="120.0" minWidth="80.0" text="ردیف" />
            <TableColumn fx:id="nameColumn" maxWidth="400.0" minWidth="300.0" prefWidth="300.0" text="نام ماده اولیه" />
            <TableColumn fx:id="categoryColumn" maxWidth="300.0" minWidth="150.0" prefWidth="150.0" text="دسته بندی" />
            <TableColumn fx:id="priceColumn" maxWidth="300.0" minWidth="220.0" prefWidth="220.0" text="قیمت هر کیلوگرم(ریال)" />
            <TableColumn fx:id="unitColumn" maxWidth="200.0" minWidth="100.0" prefWidth="100.0" text="واحد" />
            <TableColumn fx:id="numberOfUsesColumn" maxWidth="120.0" minWidth="120.0" prefWidth="120.0" text="دفعات استفاده" />
            <TableColumn fx:id="deleteRowColumn" maxWidth="90.0" minWidth="90.0" prefWidth="90.0" resizable="false" text="حذف سطر" />
         </columns>
         <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>
</TableView>

Solution

  • Not respecting RTL in table navigation is a bug that's fixed in openjfx15.

    Until then we can hack around (assuming if we are allowed to go dirty, that is use reflection to access a private field in TableViewSkin, use internal api and implementation details ;)

    • get hold of the table's skin (typically available after the table is shown) and access its internal field behavior
    • get the left/right keyMappings from the behavior's input map and remove them
    • use their respective event handlers to cross-wire key-to-handler and add them

    In code, something like (here for left/right only, need to do the same for mappings with modifiers):

    protected void hackNavigation(TableView<?> table) {
        TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
        // access private field reflectively 
        // use your own favorite utility method :)
        TableViewBehavior<?> behavior = (TableViewBehavior<?>) 
                FXUtils.invokeGetFieldValue(TableViewSkin.class, skin, "behavior");
        // access mappings
        ObservableList<Mapping<?>> mappings = behavior.getInputMap().getMappings();
        // lookup the original mappings for left/right
        KeyBinding leftBinding = new KeyBinding(KeyCode.LEFT);
        KeyBinding rightBinding = new KeyBinding(KeyCode.RIGHT);
        KeyMapping leftMapping = getMapping(mappings, leftBinding); 
        KeyMapping rightMapping = getMapping(mappings, rightBinding);
        // remove the original mappings
        mappings.removeAll(leftMapping, rightMapping);
        // create new mappings with the opposite event handlers and add them
        KeyMapping replaceRight = new KeyMapping(rightBinding, leftMapping.getEventHandler());
        KeyMapping replaceLeft = new KeyMapping(leftBinding, rightMapping.getEventHandler());
        mappings.addAll(replaceRight, replaceLeft);
    }
    
    /**
     * Utility method to get hold of a KeyMapping for a binding. 
     * Note: only use if certain that it is contained, or guard against failure 
     */
    protected KeyMapping getMapping(ObservableList<Mapping<?>> mappings, KeyBinding keyBinding) {
        Optional<KeyMapping> opt = mappings.stream()
                .filter(mapping -> mapping instanceof KeyMapping)
                .map(mapping -> (KeyMapping) mapping)
                .filter(keyMapping -> keyMapping.getMappingKey().equals(keyBinding))
                .findAny()
                ;
        return opt.get();
    }
    
    // useage
    stage.show();
    hackNavigation(myTable);