I have a TableView which contains textual and graphical content (combo boxes, check boxes etc).
When I traverse the cells using the keyboard and arrive at a cell that contains a graphic element, I would like the graphic to be selected so that I can, for example, hit F4 and have a combo list drop down, or hit the space bar and have a toggle button change state.
However, at the moment, when I TAB (or other key) to a cell, the cell containing the graphic is selected and I'm forced to use the mouse to manipulate the graphic.
How would I go about selecting the graphic element itself, rather than the cell that contains it?
IE. This is what it's doing now when I TAB into a non-textual cell:
How can I get it to do this?
I've tried several ways of getting the cell graphic but it's always null.
UPDATE:
I've done more work and can now get to the cell graphic. It was a Java newbie error. Apologies!
However, while I can now get the graphic, I still haven't been able to select or focus on it. Could anyone tell me how to do that please? Many thanks!
Here are excerpts from my updated code using combo boxes and TABbing as an example.
Key events are trapped in a generic setOnKeyPressed handler at the TableView level. Here is the code for TAB. I've indicated the places where I'm stuck.
} else if ( event.getCode() == KeyCode.TAB ) { tv.getSelectionModel().selectRightCell(); endOfRowCheck(tv, event, pos, firstCol, maxCols); event.consume(); //==> IS IT BETTER TO USE THE FOCUS MODEL OR THE SELECTION MODEL? BOTH GIVE THE CELL GRAPHIC. //==> IS THERE A BETTER WAY OF GETTING THE CELL GRAPHIC? TablePosition<S, ?> focussedPos = tv.getFocusModel().getFocusedCell(); TableColumn tableColumn = (TableColumn<S, ?>) focussedPos.getTableColumn(); TableCell cell = (TableCell) tableColumn.getCellFactory().call(tableColumn); Node cellGraphic = cell.getGraphic(); System.out.println(cellGraphic); //Output: ComboBox@44cf20e7[styleClass=combo-box-base combo-box] //==> HOW DO I NOW FOCUS ON (OR SELECT?) THE GRAPHIC? //I tried Platform.runLater() on the requestFocus but that didn't work either. cellGraphic.requestFocus(); } else if ... For completeness, here's the called endOfRowCheck method:
private void endOfRowCheck(TableView tv, KeyEvent event, TablePosition pos, TableColumn col, int maxCols) { if ( pos.getColumn() == maxCols ) { //We're at the end of a row so position to the start of the next row tv.getSelectionModel().select(pos.getRow()+1, col); event.consume(); } } I create combo box columns as follows.
In the FXML controller:
TableColumn<TestModel, DBComboChoice> colComboBoxField = DAOGenUtil.createComboBoxColumnTEST(colComboBoxField_HEADING, TestModel::comboBoxFieldProperty, arlMasterAssetClasses); In the DAOGenUtil class:
public <S> TableColumn<S, DBComboChoice> createComboBoxColumnTEST(String title, Function<S, StringProperty> methodGetComboFieldProperty, ObservableList<DBComboChoice> comboData) { TableColumn<S, DBComboChoice> col = new TableColumn<>(title); col.setCellValueFactory(cellData -> { String masterCode = methodGetComboFieldProperty.apply(cellData.getValue()).get(); DBComboChoice choice = DBComboChoice.getDescriptionByMasterCode(masterCode, comboData); return new SimpleObjectProperty<>(choice); }); col.setCellFactory(column -> ComboBoxCell.createComboBoxCell(comboData)); return col; } The ComboBoxCell class, which I use to render non-editable combos as combos and not as labels.
public class ComboBoxCell<S, T> extends TableCell<S, T> { private final ComboBox<DBComboChoice> combo = new ComboBox<>(); public ComboBoxCell(ObservableList<DBComboChoice> comboData) { combo.getItems().addAll(comboData); combo.setEditable(false); setGraphic(combo); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); combo.setOnAction((ActionEvent event) -> { try { String masterCode = combo.getSelectionModel().getSelectedItem().getMasterCode(); S datamodel = getTableView().getItems().get(getIndex()); try { Method mSetComboBoxField = datamodel.getClass().getMethod("setComboBoxField", (Class) String.class); mSetComboBoxField.invoke(datamodel, masterCode); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { System.err.println(ex); DAOGenUtil.logError(ex.getClass().toString(), ex.getMessage(), "Call to 'setComboBoxField' failed in ComboBoxCell.setOnAction for master code '" + masterCode + "'"); } } catch (NullPointerException ex) { //temporary workaround for bad test data System.out.println("caught NPE in combo.setOnAction"); } }); } public static <S> ComboBoxCell<S, DBComboChoice> createComboBoxCell(ObservableList<DBComboChoice> comboData) { return new ComboBoxCell<S, DBComboChoice>(comboData); } @Override protected void updateItem(T comboChoice, boolean empty) { super.updateItem(comboChoice, empty); if (empty) { setGraphic(null); } else { combo.setValue((DBComboChoice) comboChoice); setGraphic(combo); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } } } I'm using JavaFX8, NetBeans 8.2 and Scene Builder 8.3.
UPDATED AGAIN:
Here is a full test case as requested, reproducible in NetBeans. My apologies if it's not in an expected format ... I'm still relatively new to Java and don't know how to turn it into something you can run standalone.
If you click in the text field column and then TAB to the combo box column, the cell that contains the combo box gets the focus, not the combo box itself.
For my app, the combo boxes need to be non-editable and always rendered as combos. When the user reaches the end of a table row and hits TAB (or RIGHT ARROW), the focus needs to move to the start of the next row.
Here is the test case code.
The app:
package test; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class Test extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml")); Scene scene = new Scene(root); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } } The FXML controller:
package test; import java.net.URL; import java.util.ResourceBundle; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; public class FXMLDocumentController implements Initializable { private DAOGenUtil DAOGenUtil = new DAOGenUtil(); public ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] { testmodel.textFieldProperty(), testmodel.comboBoxFieldProperty() }); ObservableList<DBComboChoice> comboChoices = FXCollections.observableArrayList(); TableColumn<TestModel, String> colTextField = new TableColumn("text col"); TableColumn<TestModel, DBComboChoice> colComboBoxField = DAOGenUtil.createComboBoxColumn("combo col", TestModel::comboBoxFieldProperty, comboChoices); @FXML private TableView<TestModel> tv; @Override public void initialize(URL url, ResourceBundle rb) { comboChoices.add(new DBComboChoice("F", "Female")); comboChoices.add(new DBComboChoice("M", "Male")); olTestModel.add(new TestModel("test row 1", "M")); olTestModel.add(new TestModel("test row 2", "F")); olTestModel.add(new TestModel("test row 3", "F")); olTestModel.add(new TestModel("test row 4", "M")); olTestModel.add(new TestModel("test row 5", "F")); colTextField.setCellValueFactory(new PropertyValueFactory<>("textField")); tv.getSelectionModel().setCellSelectionEnabled(true); tv.setEditable(true); tv.getColumns().addAll(colTextField, colComboBoxField); tv.setItems(olTestModel); tv.setOnKeyPressed(event -> { TableColumn firstCol = colTextField; TableColumn lastCol = colComboBoxField; int firstRow = 0; int lastRow = tv.getItems().size()-1; int maxCols = 1; DAOGenUtil.handleTableViewSpecialKeys(tv, event, firstCol, lastCol, firstRow, lastRow, maxCols); }); } } The ComboBoxCell class:
package test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.TableCell; public class ComboBoxCell<S, T> extends TableCell<S, T> { private final ComboBox<DBComboChoice> combo = new ComboBox<>(); private final DAOGenUtil DAOGenUtil; public ComboBoxCell(ObservableList<DBComboChoice> comboData) { this.DAOGenUtil = new DAOGenUtil(); combo.getItems().addAll(comboData); combo.setEditable(false); setGraphic(combo); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); combo.setOnAction((ActionEvent event) -> { String masterCode = combo.getSelectionModel().getSelectedItem().getMasterCode(); S datamodel = getTableView().getItems().get(getIndex()); try { Method mSetComboBoxField = datamodel.getClass().getMethod("setComboBoxField", (Class) String.class); mSetComboBoxField.invoke(datamodel, masterCode); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { System.err.println(ex); } }); } @Override protected void updateItem(T comboChoice, boolean empty) { super.updateItem(comboChoice, empty); if (empty) { setGraphic(null); } else { combo.setValue((DBComboChoice) comboChoice); setGraphic(combo); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } } } The TableView data model:
package test; import javafx.beans.property.StringProperty; import javafx.beans.property.SimpleStringProperty; public class TestModel { private StringProperty textField; private StringProperty comboBoxField; public TestModel() { this(null, null); } public TestModel( String textField, String comboBoxField ) { this.textField = new SimpleStringProperty(textField); this.comboBoxField = new SimpleStringProperty(comboBoxField); } public String getTextField() { return textField.get().trim(); } public void setTextField(String textField) { this.textField.set(textField); } public StringProperty textFieldProperty() { return textField; } public String getComboBoxField() { return comboBoxField.get().trim(); } public void setComboBoxField(String comboBoxField) { this.comboBoxField.set(comboBoxField); } public StringProperty comboBoxFieldProperty() { return comboBoxField; } } The DBComboChoice data model:
package test; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.ObservableList; public class DBComboChoice { private StringProperty masterCode; private StringProperty masterDescription; public DBComboChoice( String masterCode, String masterDescription ) { this.masterCode = new SimpleStringProperty(masterCode); this.masterDescription = new SimpleStringProperty(masterDescription); } public String getMasterCode() { return masterCode.get(); } public StringProperty masterCodeProperty() { return masterCode; } public String getMasterDescription() { return masterDescription.get(); } public StringProperty masterDescriptionProperty() { return masterDescription; } public static DBComboChoice getDescriptionByMasterCode(String inMasterCode, ObservableList<DBComboChoice> comboData) { for ( int i=0; i<comboData.size(); i++ ) { if ( comboData.get(i).getMasterCode().equals(inMasterCode) ) { return comboData.get(i); } } return null; } @Override public String toString() { return this.masterDescription.get(); } } The DAOGenUtil class:
package test; import java.util.function.Function; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.control.ComboBox; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TablePosition; import javafx.scene.control.TableView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; public class DAOGenUtil { public <S> TableColumn<S, DBComboChoice> createComboBoxColumn(String title, Function<S, StringProperty> methodGetComboFieldProperty, ObservableList<DBComboChoice> comboData) { TableColumn<S, DBComboChoice> col = new TableColumn<>(title); col.setCellValueFactory(cellData -> { String masterCode = methodGetComboFieldProperty.apply(cellData.getValue()).get(); DBComboChoice choice = DBComboChoice.getDescriptionByMasterCode(masterCode, comboData); return new SimpleObjectProperty<>(choice); }); col.setCellFactory((TableColumn<S, DBComboChoice> param) -> new ComboBoxCell<>(comboData)); return col; } public <S> void handleTableViewSpecialKeys(TableView tv, KeyEvent event, TableColumn firstCol, TableColumn lastCol, int firstRow, int lastRow, int maxCols) { //NB: pos, at this point, is the cell position that the cursor is about to leave TablePosition<S, ?> pos = tv.getFocusModel().getFocusedCell(); if (pos != null ) { if ( event.getCode() == KeyCode.TAB ) { tv.getSelectionModel().selectRightCell(); endOfRowCheck(tv, event, pos, firstCol, maxCols); event.consume(); TablePosition<S, ?> focussedPos = tv.getFocusModel().getFocusedCell(); TableColumn tableColumn = (TableColumn<S, ?>) focussedPos.getTableColumn(); TableCell cell = (TableCell) tableColumn.getCellFactory().call(tableColumn); Node cellGraphic = cell.getGraphic(); System.out.println("node cellGraphic is " + cellGraphic); if ( cellGraphic instanceof ComboBox<?> ) { System.out.println("got a combo"); //nbg cellGraphic.requestFocus(); Platform.runLater(() -> { ((ComboBox<?>) cellGraphic).requestFocus(); }); } } else if ( ! event.isShiftDown() && ! event.isControlDown() ){ //edit the cell tv.edit(pos.getRow(), pos.getTableColumn()); } } } private void endOfRowCheck(TableView tv, KeyEvent event, TablePosition pos, TableColumn col, int maxCols) { if ( pos.getColumn() == maxCols ) { //We're at the end of a row so position to the start of the next row tv.getSelectionModel().select(pos.getRow()+1, col); event.consume(); } } } The FXML:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.TableView?> <?import javafx.scene.layout.BorderPane?> <BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="test.FXMLDocumentController"> <center> <TableView fx:id="tv" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" /> </center> </BorderPane> 
