Search code examples
javafxcontextmenukeyeventtreetableview

How to supress Key Navigation Events for TreeTableView when ContextMenu is open?


When I add a contextmenu to a treetableview, it unfortunatly still reacts on KeyEvents which are not consumed by the contextmenu.

enter image description here

For example clicking on a subitem (Item22) opening its contextmenu and then klicking the left arrow the treetableview selects the subitems parent (Item2) in the background, eventhough the contextmenu is selected and in the foreground and should (imo) consume the event, even when it cant process the event itself.

Is there a simple way to consume all navigation events, while the contextmenu is open, so the table doesnt react in the background on unused KeyNavigations? Or do you have any other suggestions on how to get the desired behavior?

Small example to demonstrate the issue:

public class TreeTableViewContextMenuKeyBehaviourMain extends Application
{
  @Override
  public void start( final Stage primaryStage )
  {
    TreeItem<Item> root = new TreeItem<>( new Item( "Root" ) );

    TreeItem<Item> item1 = new TreeItem<>( new Item( "Item1" ) );
    TreeItem<Item> item11 = new TreeItem<>( new Item( "Item11" ) );
    TreeItem<Item> item2 = new TreeItem<>( new Item( "Item2" ) );
    TreeItem<Item> item22 = new TreeItem<>( new Item( "Item22" ) );

    root.getChildren().add( item1 );
    item1.getChildren().add( item11 );
    root.getChildren().add( item2 );
    item2.getChildren().add( item22 );

    TreeTableColumn<Item, String> column = new TreeTableColumn<>( "Column" );

    column.setCellValueFactory( new TreeItemPropertyValueFactory<Item, String>( "name" ) );

    final TreeTableView<Item> treeTableView = new TreeTableView<>( root );
    treeTableView.getColumns().add( column );
    treeTableView.setShowRoot( false );
    treeTableView.setColumnResizePolicy( TreeTableView.CONSTRAINED_RESIZE_POLICY );

    treeTableView.setRowFactory( new Callback<TreeTableView<Item>, TreeTableRow<Item>>()
    {
      @Override
      public TreeTableRow<Item> call( final TreeTableView<Item> tableView )
      {
        final TreeTableRow<Item> row = new TreeTableRow<>();
        final ContextMenu rowMenu = new ContextMenu();

        rowMenu.setOnShowing( ( event ) ->
        {
          System.out.println( "OPEN!" );

          //Clear and Rebuild Contextmenu dynamically.
            rowMenu.getItems().clear();
            Menu deleteMenu = new Menu( "Delete" );
            final MenuItem deleteItem = new MenuItem( row.getItem().getName() );
            deleteMenu.getItems().add( deleteItem );
            rowMenu.getItems().add( deleteMenu );
          } );

        //If the contextmenu has no childs setOnShowing will not be called, so we need a placeholder for dynamic context menus.
        final Menu placeholder = new Menu( "Delete" );
        rowMenu.getItems().add( placeholder );

        // only display context menu for non-null items:
        row.contextMenuProperty().bind(
            Bindings.when( Bindings.isNotNull( row.itemProperty() ) ).then( rowMenu )
                .otherwise( (ContextMenu) null ) );

        return row;
      }
    } );

    BorderPane layout = new BorderPane();
    layout.setCenter( treeTableView );
    Scene scene = new Scene( layout, 400, 400 );
    scene.getStylesheets().add( getClass().getResource( "application.css" ).toExternalForm() );
    primaryStage.setScene( scene );
    primaryStage.show();
  }

  public static void main( final String[] args )
  {
    launch( args );
  }
}

public class Item
{
  private final StringProperty name = new SimpleStringProperty();

  public Item( final String name )
  {
    this.name.set( name );
  }

  public String getName()
  {
    return name.get();
  }

  public void setName( final String name )
  {
    this.name.set( name );
  }

  public StringProperty nameProperty()
  {
    return name;
  }
}

Solution

  • You can add an EventHandler to your ContextMenu to consume the KeyEvents that are attempting to funnel back up the scene graph.

    rowMenu.addEventHandler(KeyEvent.ANY, (KeyEvent t) ->
    {
        t.consume();
    });