I have an event listener that listens for keyboard events. When i try to enter edit mode by using key event, for some strange reason an incorrect cell enters edit mode.
For example I want to edit a cell. I use keyboard arrows to go to the cell I want to edit i.e. the cell that is focused. By clicking a letter on the keyboard, the focused cell should enter edit mode. When I try to edit the focused cell, the wrong cell enters edit mode.
private final class EditCell extends TableCell<SimpleStringProperty, String> implements GenericTable
{
public EditCell()
{
// Add event listsner. table is a TableView
table.setOnKeyPressed(keyEvent -> this.handleKeyPressed(keyEvent));
}
public void handleKeyPressed(KeyEvent key)
{
// Keyboard events
if (key.getCode().isLetterKey())
{
if (!this.isEditing())
{
this.edit = true;
// focus index
int focusIndex = this.table.getSelectionModel().getFocusedIndex();
this.changeTableCellFocus(this.table, focusIndex);
this.startEdit();
}
}
}
// startEdit() function
#Override
public void startEdit()
{
if (this.edit)
{
LOGGER.info("Start editing on cell index: " + this.getIndex());
super.startEdit();
this.createTextField();
this.setText(null);
this.setGraphic(this.textField);
this.textField.selectAll();
this.textField.requestFocus();
this.textField.setOnKeyPressed(keyEvent -> this.handleKeyPressed(keyEvent));
this.textField.focusedProperty()
.addListener((observable, oldValue, newValue) -> this.onTextFieldFocusChange(observable,
oldValue,
newValue));
}
}
// Change focus
public void changeTableCellFocus(final TableView<?> table, final int focusIndex)
{
table.requestFocus();
table.getSelectionModel().clearAndSelect(focusIndex);
table.getFocusModel().focus(focusIndex);
}
}
Before entering edit mode, I change focus to the clicked cell and then call the startEdit() method. I have attempted to debug the issue but with no luck. I have noticed that the focusIndex is different from the current cell index. I'm not sure why the index is different.
The problem with your code is that every cell is calling table.setOnKeyPressed(...) as it is created. This works like any other set method, so the keyPressed handler on the table is just set to the one from the last EditCell that was created. You have no control over actual creation of cells, and this is not necessarily (and unlikely) to be the cell that happens to be focused.
The TableView has enough API for you to be able to manage this directly from the table. In particular
table.getFocusModel().getFocusedCell()
will give you a TablePosition representing the currently focused cell. From that you can retrieve the corresponding row index and TableColumn. Then you just need to call table.edit(int row, TableColumn<...> column); to instruct the appropriate cell to go into editing mode.
Here's a complete example. I didn't make much effort to make the editing "pretty" in terms of selecting text etc in the text field, and you might want to implement edit cancel somehow, but this should get you started.
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewEditOnType extends Application {
#Override
public void start(Stage primaryStage) {
TableView<List<StringProperty>> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
for (int i = 0; i < 10; i++) {
table.getColumns().add(createColumn(i));
List<StringProperty> rowData = new ArrayList<>();
table.getItems().add(rowData);
for (int j = 0; j < 10 ; j++) {
rowData.add(new SimpleStringProperty(String.format("Cell [%d, %d]", i, j)));
}
}
table.setOnKeyTyped(event -> {
TablePosition<List<StringProperty>, String> focusedCell = table.getFocusModel().getFocusedCell();
if (focusedCell != null) {
table.getItems().get(focusedCell.getRow()).get(focusedCell.getColumn()).set(event.getCharacter());
table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
});
Scene scene = new Scene(new BorderPane(table), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<List<StringProperty>, String> createColumn(int colIndex) {
TableColumn<List<StringProperty>, String> col = new TableColumn<>("Column "+colIndex);
col.setCellValueFactory(cellData -> cellData.getValue().get(colIndex));
col.setCellFactory(column -> new EditCell());
return col ;
}
private static class EditCell extends TableCell<List<StringProperty>, String> {
private final TextField textField = new TextField();
EditCell() {
textProperty().bind(itemProperty());
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt -> commitEdit(textField.getText()));
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (! isNowFocused) {
commitEdit(textField.getText());
}
});
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(getItem());
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String text) {
super.commitEdit(text);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
public static void main(String[] args) {
launch(args);
}
}
Related
My question is about a strange behavior of a copound in a tableView.
The aim is to display an list of players participating to a match in a tableView. The informations displayed are the name of the player, his score, his number of successive busts and an indicator to know if it is his turn to play.
This indicator is a RadioButton as it looks better than a checkBox. When a turn comes to a player, the RadioButton will be setSelected(true), else, it'll be setSelected(false). The true or false information is given by the player's information used in the tableView. Of course, the RadioButton is in "read-only" mode.
Here is my code for the tableView :
TableView<PlayerProgressInformations> playersProgressTable = new TableView<PlayerProgressInformations>();
defineColumns(playersProgressTable);
playersProgressTable.setItems(playersAtThisTable);
for the defineColumns method :
TableColumn<PlayerProgressInformations, Boolean> colPlaying = new TableColumn<PlayerProgressInformations, Boolean>("Tour");
colPlaying.setPrefWidth(70);
TableCell<PlayerProgressInformations, Boolean>>) new RadioButton());
colPlaying.setCellValueFactory(new Callback<CellDataFeatures<PlayerProgressInformations, Boolean>, ObservableValue<Boolean>>() {
public ObservableValue<Boolean> call(CellDataFeatures<PlayerProgressInformations, Boolean> p) {
return new SimpleBooleanProperty(p.getValue().isPlaying());
}
});
colPlaying.setCellFactory(new Callback<TableColumn<PlayerProgressInformations, Boolean>, TableCell<PlayerProgressInformations, Boolean>>() {
#Override
public TableCell<PlayerProgressInformations, Boolean> call( TableColumn<PlayerProgressInformations, Boolean> param) {
RadioButtonCell<PlayerProgressInformations, Boolean> radioButtonCell =
new RadioButtonCell<PlayerProgressInformations, Boolean>();
return radioButtonCell;
}
});
And the RadioButtoCell class :
private class RadioButtonCell<S, T> extends TableCell<S,T> {
public RadioButtonCell () {
}
#Override
protected void updateItem (T item, boolean empty) {
System.out.println("Count value : "+count); //Indicator to check how many times is used the method "updateItem"
count+=1;
if (item instanceof Boolean) {
Boolean myBoolean = (Boolean) item;
if (!empty) {
System.out.println("Valeur du boolean : "+item);
RadioButton radioButton = new RadioButton();
radioButton.setDisable(true);
radioButton.setSelected(myBoolean);
radioButton.setStyle("-fx-opacity: 1");
setGraphic(radioButton);
}
}
}
}
The stranges behaviors are the ones bellow :
Problem 1 : When I join a first player to the game's table, the updateItem methode is called 17 times.
If a second one is joining, this number for the first player increase to 57, or 60 and 17 for the second player.
Finally, if a third one is joining, it is 90 times for the first player, 57 or 60 for the second one and 17 for the third one.
Why does this method is so often called ? And why those specific numbers ?
More over, after this "initialization" the method is called 2 times more after each turn as I expected : one time to unselect a RadioButton and one time to select the next one.
Problem 2 : When a first player join the table, he is of course the first to play and, on his screen, the RadioButton is selected.
When a second player join the table, this second player see a RadioButton selected for the first player and a RadioButton unselected for himself. That's the behavior expected. But for the first player, the 2 RadioButtons are unselected.
And if a 3rd player join the table, he'll see the RadioButton selected for the first player and unselected for himself and the 2nd player. This is also the result expected. But, for the second and the first players, all of the 3 RadioButtons are unselected.
Why this strange behavior ? More over, after a first turn, all the RadioButtons appears selected or unselected as expected as if the bug disappeared.
Could you help me to understand what is happening and how to solve those bugs ?
Thank you very much
Sample Code
Here is a table implementation that works. I guess you could compare it to your implementation to see what the differences are. Probably the main "fix" is the updateItem handling of empty and null values.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
public class PlayerViewer extends Application {
private final ObservableList<Player> data =
FXCollections.observableArrayList(
new Player("Jacob", true),
new Player("Isabella", false),
new Player("Ethan", true)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
TableView<Player> table = new TableView<>(data);
table.setPrefHeight(130);
table.setPrefWidth(150);
TableColumn<Player, String> handleCol = new TableColumn<>("Handle");
handleCol.setCellValueFactory(new PropertyValueFactory<>("handle"));
table.getColumns().add(handleCol);
TableColumn<Player, Boolean> playingCol = new TableColumn<>("Playing");
playingCol.setCellValueFactory(new PropertyValueFactory<>("playing"));
playingCol.setCellFactory(param -> new TableCell<>() {
RadioButton indicator = new RadioButton();
{
indicator.setDisable(true);
indicator.setStyle("-fx-opacity: 1");
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Boolean isPlaying, boolean empty) {
super.updateItem(isPlaying, empty);
if (empty || isPlaying == null) {
setGraphic(null);
} else {
indicator.setSelected(isPlaying);
setGraphic(indicator);
}
}
});
table.getColumns().add(playingCol);
stage.setScene(new Scene(table));
stage.show();
}
public static class Player {
private final SimpleStringProperty handle;
private final SimpleBooleanProperty playing;
private Player(String handle, boolean playing) {
this.handle = new SimpleStringProperty(handle);
this.playing = new SimpleBooleanProperty(playing);
}
public SimpleStringProperty handleProperty() {
return handle;
}
public String getHandle() {
return handle.get();
}
public void setHandle(String handle) {
this.handle.set(handle);
}
public SimpleBooleanProperty playingProperty() {
return playing;
}
public boolean isPlaying() {
return playing.get();
}
public void setPlaying(boolean playing) {
this.playing.set(playing);
}
}
}
Additional comments on your question
In terms "Problem 1", of how many times updateItem is called, that's an internal toolkit thing, your code shouldn't really care about that, it just needs to make sure that whenever it is called, that it does the right thing.
Regarding your "Problem 2", regarding the interaction of multiple views for multiple players, who knows? Impossible to say without further additional code, which would probably end up making this question too broad anyway. If you have a specific question about how to handle the interaction of displays for multiple players you will need to expand and clarify your question (likely as a new question with a mcve).
For your implementation, I would advise restyling the radio button (via CSS), so that it doesn't look like a standard user-selectable radio button (because you have disabled the selection capability and then removed the default disabled opacity setting). Or, you could use a custom indicator control such as the Bulb class from this answer, which might be preferred.
Here's another sample that works. (I was almost done with it when #jewelsea posted, so figured I would go ahead and post it anyway. It is similar but the differences between the two might be useful.)
The main issues in your code that I can see are:
Your cell value factory returns a new BooleanProperty every time it is invoked. This means the cell can't observe the correct property, and has no opportunity to update itself when the value changes. You should use JavaFX properties in your model class so that the cell can observe a single property.
Your cell implementation doesn't call the superclass implementation of updateItem(...). This means basic functionality won't happen, so updates may or may not occur when they are needed, and selection won't work, etc.
Your cell implementation doesn't deal with empty cells, so those will not necessarily display correctly.
In this implementation I used a Game class to keep the "current player" and gave each player a reference to the (same) game instance. The playing property in the Player is exposed as a read-only property and its value is bound to the game's current player. This means that only one player can have playing==true, without lots of "wiring" between the players.
The buttons allow for testing adding new players (who are automatically set as the "current" player) or moving the current player to the next player.
import java.util.Iterator;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class PlayerTable extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Player> table = new TableView<>();
TableColumn<Player, String> playerCol = new TableColumn<>("Name");
playerCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
TableColumn<Player, Boolean> playingColumn = new TableColumn<>("Playing");
playingColumn.setCellValueFactory(cellData -> cellData.getValue().playingProperty());
playingColumn.setCellFactory(tc -> new RadioButtonTableCell<>());
table.getColumns().add(playerCol);
table.getColumns().add(playingColumn);
Game game = new Game();
Button newPlayerButton = new Button("New Player");
newPlayerButton.setOnAction(e -> addNewPlayer(game, table.getItems()));
Button nextPlayerButton = new Button("Next player");
nextPlayerButton.setOnAction(e -> selectNextPlayer(game, table.getItems()));
HBox controls = new HBox(5, newPlayerButton, nextPlayerButton);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
BorderPane root = new BorderPane();
root.setCenter(table);
root.setBottom(controls);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addNewPlayer(Game game, List<Player> players) {
int playerNumber = players.size() + 1 ;
Player newPlayer = new Player(game, "Player "+playerNumber);
game.setCurrentPlayer(newPlayer);
players.add(newPlayer);
}
private void selectNextPlayer(Game game, List<Player> players) {
if (players.isEmpty()) return ;
for (Iterator<Player> i = players.iterator() ; i.hasNext() ;) {
if (i.next() == game.getCurrentPlayer()) {
if (i.hasNext()) {
game.setCurrentPlayer(i.next());
} else {
game.setCurrentPlayer(players.get(0));
}
return ;
}
}
game.setCurrentPlayer(players.get(0));
}
public static class RadioButtonTableCell<S> extends TableCell<S, Boolean> {
private RadioButton radioButton ;
public RadioButtonTableCell() {
radioButton = new RadioButton();
radioButton.setDisable(true);
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
radioButton.setSelected(item);
setGraphic(radioButton);
}
}
};
public static class Game {
private final ObjectProperty<Player> currentPlayer = new SimpleObjectProperty<>() ;
public ObjectProperty<Player> currentPlayerProperty() {
return currentPlayer ;
}
public Player getCurrentPlayer() {
return currentPlayerProperty().get();
}
public void setCurrentPlayer(Player player) {
currentPlayerProperty().set(player);
}
}
public static class Player {
private final String name ;
private final ReadOnlyBooleanWrapper playing ;
public Player(Game game, String name) {
this.name = name ;
playing = new ReadOnlyBooleanWrapper() ;
playing.bind(game.currentPlayerProperty().isEqualTo(this));
}
public String getName() {
return name ;
}
public ReadOnlyBooleanProperty playingProperty() {
return playing.getReadOnlyProperty() ;
}
public boolean isPlaying() {
return playingProperty().get();
}
}
public static void main(String[] args) {
launch(args);
}
}
We try to achieve the following:
When a node gets selected in a JavaFX TreeTableView, also "the path to the root", i.e., the parent, the grandparent, and so on should get selected. Selected in this case means highlighted with a different background color, see the image (in the example, the node on Level 2 has been clicked by the user).
Is there a built-in function how to achieve this?
We tried using CSS but did not succeed.
There's no "built-in function" to do this. Use a row factory on the tree table view to create rows that observe the selected item, and set a pseudoclass on the row accordingly.
For example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectionPath extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<Item>();
PseudoClass ancestorOfSelection = PseudoClass.getPseudoClass("ancestor-of-selection");
table.setRowFactory(ttv -> new TreeTableRow<Item>() {
{
table.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> updateStyleClass());
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
updateStyleClass();
}
private void updateStyleClass() {
pseudoClassStateChanged(ancestorOfSelection, false);
TreeItem<Item> treeItem = table.getSelectionModel().getSelectedItem();
if (treeItem != null) {
for (TreeItem<Item> parent = treeItem.getParent() ; parent != null ; parent = parent.getParent()) {
if (parent == getTreeItem()) {
pseudoClassStateChanged(ancestorOfSelection, true);
break ;
}
}
}
}
});
TreeTableColumn<Item, String> itemCol = new TreeTableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getValue().getName()));
table.getColumns().add(itemCol);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
table.getColumns().add(valueCol);
table.setRoot(createRandomTree());
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree() {
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", 0));
Random rng = new Random();
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= 20 ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final String name ;
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
this.name = name ;
setValue(value);
}
public String getName() {
return name ;
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Now you can just style the "ancestor of a selected node" in CSS:
File style.css:
.tree-table-row-cell:ancestor-of-selection {
-fx-background: -fx-selection-bar;
-fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
}
(You may want to modify the CSS to get better control, e.g. set different colors for selected rows in a non-focused table, etc. See the default stylesheet for details on the default style.)
Here's a screenshot of the above test app:
How can I make adding a value to items in a combo box possible so the user can either select from the existing items or clique "Add element" item to add a new item?
private ComboBox<String> comboStructDonnees;
Followed by:
comboData.getItems().addAll("TVW", "VWT", "TTVW", "VWXT", "Add item");
I don't know which event should I create next, I want to the text to be entered on the added element if possible.
Any help would be appreciated.
You can add an item with a "special value" (e.g. an empty string) to the end of the list of items for the combo box.
Use a cell factory to create a cell that displays a user-friendly message ("Add item..", for example) to the user when that value is displayed. Add an event filter to the cell that displays a dialog for inputting a new value if the cell is displaying the special value.
Here's a quick SSCCE:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextInputDialog;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AddItemToComboBox extends Application {
#Override
public void start(Stage primaryStage) {
ComboBox<String> combo = new ComboBox<>();
combo.getItems().addAll("One", "Two", "Three", "");
combo.setCellFactory(lv -> {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
if (item.isEmpty()) {
setText("Add item...");
} else {
setText(item);
}
}
}
};
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, evt -> {
if (cell.getItem().isEmpty() && ! cell.isEmpty()) {
TextInputDialog dialog = new TextInputDialog();
dialog.setContentText("Enter item");
dialog.showAndWait().ifPresent(text -> {
int index = combo.getItems().size()-1;
combo.getItems().add(index, text);
combo.getSelectionModel().select(index);
});
evt.consume();
}
});
return cell ;
});
BorderPane root = new BorderPane();
root.setTop(combo);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I would like to add a handler to the ComboBox inside a ComboBoxTableCell in a TableView in JavaFX8. I can see that there is a private ComboBox value in the ComboBoxTableCell class, yet I have no idea how to access it. I tell the column to use a ComboBoxTableCell via the setCellFactory method. Is there any way to get the ComboBox?
EDIT: I want to add a listener to the ComboBox which enables choosing items by entering keys. I already have one for a normal ComboBoxand I would like to reuse the same for the ComboBox in the TableCell.
To my knowledge there's no way to get the reference to the ComboBox of the ComboBoxTableCell. If that's true, it's not possible to add a listener to it.
An alternate approach would be to create your own custom cell containing a ComboBox. With this approach, you can manipulate the ComboBox in any way you'd like.
import java.util.function.BiConsumer;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class ComboBoxTable extends Application {
#Override
public void start(Stage stage) {
int numOfCols = 2;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 10; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
final int j = i;
// The fourth argument in the method, the BiConsumer, might require
// an explanation. Basically we are saying that when the BiConsumer
// are given an ObservableList<String> and a String, we set the
// value of the String as the value of the element at position "j"
// of the row, where "j" will be the column index.
table.getColumns().add(addComboBoxColumn(i, "Column " + i, row -> new SimpleStringProperty(row.get(j)),
(row, newText) -> row.set(j, newText)));
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a TableColumn with ComboBoxCells.
*/
private TableColumn<ObservableList<String>, String> addComboBoxColumn(int index, String name,
Function<ObservableList<String>, ObservableValue<String>> property,
BiConsumer<ObservableList<String>, String> updater) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(e -> new ComboBoxCell(updater, index));
return col;
}
/**
* A TableCell with a ComboBox in it.
*/
public class ComboBoxCell extends TableCell<ObservableList<String>, String> {
private ComboBox<String> comboBox = new ComboBox<String>();
/**
* #param updater
* The updater makes sure that the cell value corresponds
* with the value in the ComboBox.
* #param colIndex
* The index of this column.
*/
public ComboBoxCell(BiConsumer<ObservableList<String>, String> updater, int colIndex) {
comboBox.setEditable(true);
comboBox.getEditor().textProperty().addListener((old, oldValue, newValue) -> {
if (getIndex() >= 0) {
// We provide the BiConsumer.accept() with an
// ObservableList<String> and a String. The BiConsumer will
// do the operation specified in the definition we provided
// in addColumn() using these two objects.
updater.accept(getTableView().getItems().get(getIndex()), (String) newValue);
}
});
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
// If we don't check if this value is the same as the old one,
// the cursor is moved to the beginning of the editor every time
// anything is typed.
if (!item.equals(comboBox.getEditor().getText())) {
comboBox.getEditor().setText(item);
}
setGraphic(comboBox);
}
}
}
public static void main(String[] args) {
launch();
}
}
an other possible solution is to add a StringConverter<>() to your ComboBoxTableCell<R, T>() for correct String display of an object and add a changeListener to your ObjectProperty
TableView<ParentObject> table = new TableView<>();
table.setEditable(true);
TableColumn<ParentObject, Item> col = new TableColumn<>("name");
col.setEditable(true);
col.setCellValueFactory(cellData -> {
ObjectProperty prop = new SimpleObjectProperty<Item>(cellData.getValue().getItem());
prop.addListener((observable, oldVal, newVal) -> {
//do something for example set the Object in getValue()
cellData.getValue().setItem(newVal);
});
return prop;
});
final StringConverter<Item> converter = new StringConverter<>() {
#Override
public Item fromString(final String string) {
return null;
}
#Override
public String toString(final Item object) {
return object.getLabel();
}
};
// get your list of possible Objects to swap
List<Item> itemList = Database.getAllItems();
col.setCellFactory(c -> new ComboBoxTableCell(converter, FXCollections.observableArrayList(itemList)));
table.getColumns().add(col);
The Object SimpleObjectProperty<> can be replaced with other javafx.beans.properties for Example StringProperty, IntegerProperty, BooleanProperty or ListProperty
I'm aware that this isn't the 100% matching answer to the question but this helped me in my issue searching for the right answer to the problem
I've tried everything. I think they made a big mistake not giving any reference to the indexed cell in anything.
I can get my menu, but not in the right place. Right click is fine.
In my TreeView I can use get KeyReleased but I don't know where to put the menu.
setOnKeyReleased((KeyEvent t) -> {
switch (t.getCode()) {
case CONTEXT_MENU:
getSelectionModel().getSelectedItem().setGraphic(new Label("hi"));
//showMenu just calls show on my ContextMenu of my subclass TreeNode
((TreeNode)getSelectionModel().getSelectedItem()).showMenu(
getSelectionModel().getSelectedItem().getGraphic().getLocalToSceneTransform());
break;
}
});
None of the layout methods will give me the coords of the TreeCell
It simply isn't possible to provide API access to the cell for a given item. Not every item has a cell associated with it. On top of that, the item which is represented by a cell may change at any time, so even if you could provide access to the cell, the API would potentially be very confusing.
The basic trick to anything like this is to create a cell factory, and register the appropriate listeners with the cell. Your case is somewhat tricky, but possible. The following works to get the cell representing the selected item (you may want to modify the code somewhat to deal with the case where the cell is scrolled off the screen).
(Note that I used the Z key, arbitrarily, as I don't have a ContextMenu key on my laptop.)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
TreeView<String> treeView = new TreeView<>();
TreeItem<String> treeRoot = new TreeItem<>("Root");
for (int i=1; i<=5; i++) {
TreeItem<String> child = new TreeItem<>("Item "+i);
child.getChildren().addAll(new TreeItem<>("Item "+i+"A"), new TreeItem<>("Item "+i+"B"));
treeRoot.getChildren().add(child);
}
treeView.setRoot(treeRoot);
root.setCenter(treeView);
ObjectProperty<TreeCell<String>> selectedCell = new SimpleObjectProperty<>();
treeView.setCellFactory(tree -> {
TreeCell<String> cell = new TreeCell<>();
cell.textProperty().bind(cell.itemProperty());
ChangeListener<TreeItem<String>> listener = (obs, oldItem, newItem) -> {
TreeItem<String> selectedItem = treeView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
selectedCell.set(null);
} else {
if (selectedItem == cell.getTreeItem()) {
selectedCell.set(cell);
}
}
};
cell.treeItemProperty().addListener(listener);
treeView.getSelectionModel().selectedItemProperty().addListener(listener);
return cell ;
});
ContextMenu contextMenu = new ContextMenu();
for (int i=1; i<=3; i++) {
String text = "Choice "+i;
MenuItem menuItem = new MenuItem(text);
menuItem.setOnAction(event -> System.out.println(text));
contextMenu.getItems().add(menuItem);
}
treeView.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.Z) {
if (selectedCell.get() != null) {
Node anchor = selectedCell.get();
// figure center of cell in screen coords:
Bounds anchorBounds = anchor.getBoundsInParent();
double x = anchorBounds.getMinX() + anchorBounds.getWidth() / 2 ;
double y = anchorBounds.getMinY() + anchorBounds.getHeight() / 2 ;
Point2D screenLoc = anchor.getParent().localToScreen(x, y);
contextMenu.show(selectedCell.get(), screenLoc.getX(), screenLoc.getY());
}
}
});
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}