mike rodent
mike rodent

Reputation: 15632

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.

Upvotes: 1

Views: 433

Answers (3)

Ingo
Ingo

Reputation: 635

The given answer covers only part of the problem: e.g. if you click with the mouse behind the last column and press F2 then startEdit will still throw the NPE.

I solved this problem in my project by installing a focus listener which selects the default column whenever there is no valid column focused (for whatever reason):

//guarantee that always a valid column is selected to avoid NPE during startEdit
tableView.getFocusModel().focusedCellProperty().addListener((observable, oldValue, newValue) -> {
  if (newValue.getTableColumn() == null){
    tableView.getSelectionModel().select(newValue.getRow(), defaultColumn);
  }
});

E.g. if defaultColumn should be the first column you can set

defaultColumn = tableView.getColumns.get(0)

Upvotes: 1

Slaw
Slaw

Reputation: 45756

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.

Upvotes: 2

mike rodent
mike rodent

Reputation: 15632

Found the answer (or an answer). There is in fact a focus system internal to the TreeTableView... see getFocusModel(). It appears that changes in this are not picked up by Stage's focus owner mechanism:

stage.getScene().focusOwnerProperty().addListener( new ChangeListener(){
...

So what you have to do is make the internal focus model change whenever selection changes:

treeTableView.getSelectionModel().getSelectedItems().addListener(new ListChangeListener() {
    void onChanged(ListChangeListener.Change c) {
        TreeItem newSelectedItem = c.list.get(0);
        int row = treeTableView.getRow( newSelectedItem );
        treeTableView.getFocusModel().focus( row, treeTableView.getColumns().get(0) );
    }
}

There are one or two points to be made about this:

  1. I'm not entirely sure that c.list.get( 0 ) can always be guaranteed to deliver the newly selected item... and this is particularly true if selection mode is multiple selection
  2. I'm slightly surprised to find that even if you click on a cell in one of the table columns (i.e. columns 1, 2, 3...), as opposed to the tree column (column 0), you always seem to have a TreeItem from column 0 returned. In a normal table you can of course select a block of cells at random. I'm wondering now whether you can do this with this control, or whether all selection is really "row selection".
  3. I would have thought that the above functionality should be included in the default implementation of the class: surely it's not terribly good if a simple keyboard operation by someone who either hates using or can't use the mouse provokes a JavaFX exception (and also means that the edit operation doesn't happen).

Upvotes: 0

Related Questions