1

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:

enter image description here

How can I get it to do this?

enter image description here

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> 
3
  • Please provide a minimal reproducible example that demonstrates the problem. Commented Aug 14, 2018 at 8:29
  • @kleopatra I've updated my post and provided a full test case as requested. Many thanks for your interest and help. Commented Aug 14, 2018 at 22:48
  • hmm.. it's halfway working in fx10: the navigation sequence is a slightly broken (which seems to be a bug in core) Commented Aug 15, 2018 at 8:51

1 Answer 1

1

Your question is too broad, you are trying to tackle too many problems at once, f.i.:

  • change navigation sequence, that is endOfRow handling
  • use tab for navigation
  • implement a custom cell
  • map visual appearence of a value to something with meaning for the user
  • start editing (aka: "focus") on reaching a cell

The most important (IMO) misconception in your code is the last bullet: you must not by-pass the editing mechanism when changing the underlying data. So change thinking "focus the graphic" to "start edit".

Below is a stand-alone example that demonstrates how to start with core support and modify it to get nearer to what's really needed. It

  • configures a core ComboBoxTableCell with a (crude ;) StringConverter to do the mapping of masterCode -> masterDescription
  • extends that core cell to request focus in startEdit
  • registers a listener on the table's (actually its focusModel's) focusedCell property that starts editing the new cell

Options to proceed from here:

  • to show the editable control always, look at the code of ComboBoxTableCell and modify to show the combo always
  • re-apply the tab handling (looks fine to me)
  • change navigation sequence as needed

The code:

public class TableCellFocusApp extends Application { private Parent createContent() { ObservableList<TestModel> olTestModel = FXCollections .observableArrayList(testmodel -> new Observable[] { testmodel.textFieldProperty(), testmodel.comboBoxFieldProperty() }); TableView<TestModel> table = new TableView<>(); 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")); TableColumn<TestModel, String> colTextField = new TableColumn<>("text col"); colTextField .setCellValueFactory(cb -> cb.getValue().textFieldProperty()); TableColumn<TestModel, String> gender= new TableColumn<>("Gender"); gender.setMinWidth(100); gender.setCellValueFactory(cb -> cb.getValue().comboBoxFieldProperty()); StringConverter<String> converter = new StringConverter<>() { @Override public String toString(String object) { return "F".equals(object) ? "Female" : "Male"; } @Override public String fromString(String string) { return "Female".equals(string) ? "F" : "M"; } }; gender.setCellFactory(cb -> new ComboBoxTableCell<>(converter, "F", "M") { @Override public void startEdit() { super.startEdit(); if (getGraphic() != null) { getGraphic().requestFocus(); } } }); // just to see that the data is updated correctly - add a readonly column TableColumn<TestModel, String> plainGender = new TableColumn<>("readonly"); plainGender.setCellValueFactory(cb -> cb.getValue().comboBoxFieldProperty()); plainGender.setEditable(false); table.getFocusModel().focusedCellProperty().addListener((src, ov, nv) -> { if (nv != null && nv.getTableColumn() == gender) { table.edit(nv.getRow(), gender); } }); table.getSelectionModel().setCellSelectionEnabled(true); table.setEditable(true); table.getColumns().addAll(colTextField,gender, plainGender ); //, colComboBoxField ); table.setItems(olTestModel); BorderPane content = new BorderPane(table); return content; } @Override public void start(Stage stage) throws Exception { stage.setScene(new Scene(createContent())); stage.setTitle(FXUtils.version()); stage.show(); } public static void main(String[] args) { launch(args); } @SuppressWarnings("unused") private static final Logger LOG = Logger .getLogger(TableCellFocusApp.class.getName()); } 
Sign up to request clarification or add additional context in comments.

1 Comment

Brilliant, kleopatra, thank you. Your point re me confusing "focussing on the graphic" rather than "starting the edit" was spot on. I added the startEdit to the ComboBoxCell class and changed the TAB key handler to edit the cell rather than requestFocus and it worked perfectly in my test case. Now I just have to get it working in my app. Thanks again.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.