Search code examples
javajavafxtreeviewedittreetableview

TreeView/TreeTableView - KeyEvent F2 causes JavaFX internal NPE


Having put a "standard" editor on column 0 of a TreeTableView by doing this:

treeTableView.columns.get(0).setCellFactory( TextFieldTreeTableCell.forTreeTableColumn());

... it's nice to find that a couple of things start an edit of a TreeItem: one is to click the cell, and another is to press F2.

To my slight dismay, however, if I start up the app and, without having selected a TreeItem using the mouse, but having programmatically selected the first child of the root TreeItem, if I then (possibly after navigating using keyboard keys only) press F2, the JavaFX internals throw an NPE, which looks like this:

java.lang.NullPointerException: null
    at com.sun.javafx.scene.control.behavior.TableViewBehaviorBase.activate(TableViewBehaviorBase.java:890)
    at com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$KeyHandler.process(Scene.java:4058)
    at javafx.scene.Scene$KeyHandler.access$1500(Scene.java:4004)
    at javafx.scene.Scene.processKeyEvent(Scene.java:2121)
    at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2595)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:217)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:149)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:248)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:390)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:247)
    at com.sun.glass.ui.View.handleKeyEvent(View.java:547)
    at com.sun.glass.ui.View.notifyKey(View.java:971)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)

It can't be that the mouse puts the focus directly on the TreeItem because TreeItem's super class is Object. I put a focus owner listener on the Scene, which confirms that nothing focus-related changes when you click on the TreeTableView. And yet, qualitatively, something does change.

Maybe it's something to do with Behavior, InputMap, "skins", or any one of many mysterious JavaFX things of which I am ignorant.

Under these circumstances, it seems to me that one option might be to intercept the F2 keystroke somehow, and prevent the default functioning, and also programmatically instigate editing of the TreeItem which is selected but which doesn't have focus.

NB I put a "key pressed" and "key released" event listeners on the TreeTableView. These included the following line:

TreeTablePosition pos = treeTableView.getFocusModel().getFocusedCell();

The "anomalous" event I'm describing, where the JavaFX internals throw an NPE, is characterised by, in the "key release" listener, pos.col == -1 and pos.getTableColumn() == null. I can also tell, from logging, that the NPE happens before the "key released" handler responds.

For information, the "key pressed" listener never logs anything when I press F2 without having first mouse-clicked a TreeItem (due no doubt to the JavaFX internal Exception killing off the normal "broadcast" to listeners), leading me to suppose that the JavaFX framework also precedes any user-added key-press listener.


Solution

  • If you select your cell using TableSelectionModel#select(int,TableColumnBase) then the NPE does not occur.

    Selects the cell at the given row/column intersection. If the table control is in its 'cell selection' mode (where individual cells can be selected, rather than entire rows), and if the column argument is null, this method should select all cells in the given row.

    For example:

    TreeTableColumn col = treeTableView.getColumns().get(0);
    treeTableView.getSelectionModel().select(rowIndex, col);
    

    With the above you don't have to worry about the focus model. The reason for that is because the default selection model implementation will focus the given row+column (i.e. cell) for you (not sure if that's guaranteed behavior but it's certainly polite behavior).

    Basically, your current code caused a row to be selected/focused but not a specific cell in that row. Thus when you tried to enter "edit mode" there was no way to determine which cell to target. I would consider the NPE to be a bug. Not sure what's meant to happen when no column is selected/focused, but the control's behavior should handle that case gracefully.

    If interested, below was my debugging process.


    Here's the code responsible for the NPE:

    protected void activate(KeyEvent e) {
        TableSelectionModel sm = getSelectionModel();
        if (sm == null) return;
    
        TableFocusModel fm = getFocusModel();
        if (fm == null) return;
    
        TablePositionBase<TC> cell = getFocusedCell();
        sm.select(cell.getRow(), cell.getTableColumn());
        setAnchor(cell);
    
        // check if we are editable
        boolean isEditable = isControlEditable() && cell.getTableColumn().isEditable();
    
        // edit this row also
        if (isEditable && cell.getRow() >= 0) {
            editCell(cell.getRow(), cell.getTableColumn());
            e.consume();
        }
    }
    

    Specifically, this is the problematic line:

    boolean isEditable = isControlEditable() && cell.getTableColumn().isEditable();
    

    Which means either cell is null or getTableColumn() is returning null. After some debugging with the help of JEP 358: Helpful NullPointerExceptions we know it's the latter:

    Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "javafx.scene.control.TableColumnBase.isEditable()" because the return value of "javafx.scene.control.TablePositionBase.getTableColumn()" is null
        at javafx.controls/com.sun.javafx.scene.control.behavior.TableViewBehaviorBase.activate(TableViewBehaviorBase.java:898)
    

    If getTableColumn() is returning null then that means that, as far as the focus model is concerned, there is no column focused and thus no specific cell is focused. The fix is to select/focus a specific row+column (i.e. cell) and not just an entire row.