Strange behavior of a RadioButton in a TableView - java

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);
}
}

Related

change BackGround color of specific elements in a listView (JavaFx)

i'm working on a project and i'd like to find a way to change the background color of some elements in a listView. i've find a way to add css style class to the listView in general but not to specific elements .
Also , i've heard about cell factory but I dont know if cell factory can adapt during the programme or just set up things at the begging
(i have a listView of an object that I call player , and I want that , when the player in the listView get enough points , his name becomes red)
is there a way to do something like this ?
ListView<Players> listview = ...;
for(Player p : listView){
p.addListener(//change color to red)
}
Thanks
I would use ObservableList and addListener to the given List.
Sample code:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application {
private StackPane main;
private ListView<Player> players;
#Override
public void start(Stage stage) {
main = new StackPane();
var scene = new Scene(main, 640, 480);
players = new ListView<Player>();
ObservableList<Player> playerObjs = FXCollections.observableArrayList (
new Player("A", 50),
new Player("B", 30),
new Player("C", 60),
new Player("D", 5),
new Player("E", 0)
);
players.setItems(playerObjs);
playerObjs.addListener(new ListChangeListener<Player>() {
#Override
public void onChanged(Change<? extends Player> change) {
updateView();
}
});
main.getChildren().add(players);
stage.setScene(scene);
stage.show();
}
public void updateView() {
for(int i = 0; i < players.getItems().size(); i++) {
if(players.getItems().get(i).getHp() < 10) {
players.getItems().get(i).setBackground(...);
}
}
}
public static void main(String[] args) {
launch();
}
}
Now, everytime the list changes, it calls updateView(), which if some condition holds, will set the given item Background to some value.
Let me know if that helped.

How to set OnClick or OnSelection for ControlsFX CustomTextField autocomplete dropdown?

I am using ControlsFX's CustomTextField. When I click on one of the autocomplete options, I need to clear the TextField and create a Tag so I can add it to a FlowPane. How do I set up an OnClick or OnSelectionChange listener or override the current OnClick?
I took a look at the CustomTextField documentation and I can't find a clear way of doing what you want. So I will guess you have to implement it yourself or to find a workaround. In case you decide to choose the second choice here is something that I believe works very well :
import java.util.ArrayList;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestApplication extends Application {
private boolean addedBySelection = false;
private ArrayList<String> tagList = new ArrayList<>();
private FlowPane tagPane;
#Override
public void start(Stage primaryStage) throws Exception {
VBox mainPane = new VBox(10);
mainPane.setStyle("-fx-background-color : white");
mainPane.setPadding(new Insets(15));
mainPane.setAlignment(Pos.CENTER);
tagPane = new FlowPane(15, 10);
tagPane.setPrefHeight(50);
CustomTextField field = new CustomTextField() {
#Override
public void paste() {
super.paste();
addedBySelection = false;
}
};
field.setOnKeyPressed(e -> {
addedBySelection = false;
});
field.setOnKeyReleased(e -> {
addedBySelection = true;
});
field.textProperty().addListener(e -> {
if (addedBySelection) {
System.out.println("Text Changed from the suggession list ");
addTag(field.getText());
addedBySelection = false;
field.clear();
addedBySelection = true;
} else {
System.out.println("User Input (Mouse paste, or typing) ");
}
});
TextFields.bindAutoCompletion(field, new String[] { "Java", "C++", "C#", "Python", "Haskell" });
mainPane.getChildren().addAll(field, tagPane);
Scene scene = new Scene(mainPane, 200, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addTag(String tag) {
if (!tagList.contains(tag)) {
tagList.add(tag);
Label tagLabel = new Label(tag);
tagLabel.setStyle("-fx-background-color : #E1ECF4; -fx-text-fill : #6A739D;");
tagPane.getChildren().add(tagLabel);
}
}
public static void main(String[] args) {
launch(args);
}
}
I tried to keep it a simple as it could. The code above is doing exactly what you are after. The logic is to set a listener on the textProperty ( cause we can't set one on selection with the mouse from the autocomplete list ) and somehow find out if the user actually triggers the event using the autocomplete list or not. Thus I have a flag looking for user inputs (ex. key press) and 'releasing' the flag each time the keys are released. We have to catch the paste action as well in order to avoid mistakes if the user pastes a text on the field. One more last thing is the way we clear the field. We have to set our flag to false because the field.clear() will trigger an event as well and we don't want to fall into an event loop.
Note : With the current workaround, you will see that you are able to make a selection from the autocomplete list by pressing the enter key as well.

SelectedItems empty if multiple rows selected using different columns

I have a TableView in SelectionMode.MULTIPLE. Using a ListChangeListener I'm able to catch the selection of multiple rows (by pressing Shift).
However my solution only works if the items are being selected in the same column OR in the area without columns. Gif for illustration with 4 examples:
OK: Selecting 3 items using Shift in State column
OK: Selecting 4 items using Shift in Idx column
OK: Selecting 4 items using Shift starting from State column to area without columns
Error: Trying to select 4 items using Shift starting from State column to Data Item column
The problem seems to be that the SelectedItems-list is apparently empty in the last example. I'd really appreciate your help regarding this issue.
Here is my approach:
ObservableList<DataRowModel> dataRows = FXCollections.observableArrayList();
dataRows.addAll(dataSetModel.getRows());
tableDataRow.setItems(dataRows);
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
System.out.println(c.getList()); //Empty [] when selected using different columns
}
});
Just for a better understanding of my code: setSelected(...) sets a BooleanProperty on my DataRowModel which is bound to the State-Column.
Without context the reason for using this selected-property seems to be quite silly. However, there are various other fragments of code with ChangeListeners bound to the selected-property.
SSCCE ready to run:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<DataRowModel> tableDataRow = new TableView<DataRowModel>();
private TableColumn<DataRowModel, String> tableDataRowNameColumn = new TableColumn<>("Data Item");
private TableColumn<DataRowModel, String> tableDataRowStateColumn = new TableColumn<>("State");
private final ObservableList<DataRowModel> dataRows =
FXCollections.observableArrayList(
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(500);
stage.setHeight(500);
tableDataRow.setItems(dataRows);
tableDataRowNameColumn.setCellValueFactory(f -> f.getValue().getName());
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
}
});
tableDataRow.getColumns().addAll(tableDataRowNameColumn, tableDataRowStateColumn);
((Group) scene.getRoot()).getChildren().addAll(tableDataRow);
stage.setScene(scene);
stage.show();
}
public static class DataRowModel {
private StringProperty name = new SimpleStringProperty(this, "name", "");
private BooleanProperty selected = new SimpleBooleanProperty(this, "selected", true);
private StringProperty state = new SimpleStringProperty(this, "state", "");
public DataRowModel(String name, boolean selected) {
this.name.setValue(name);
this.selected.setValue(selected);
this.selected.addListener((observable, oldVal, newVal) -> {
getState(); // Refresh State value
});
}
public StringProperty getName() {
return name;
}
public BooleanProperty isSelected() {
return selected;
}
public void setSelected(boolean selected) {
if (this.selected.getValue() != selected)
this.selected.setValue(selected);
}
public StringProperty getState() {
String stateStr = "";
if (selected.getValue())
stateStr += "Selected";
state.setValue(stateStr);
return state;
}
}
}
I was able to generate this by editing the Oracle's Person tableview example.
This is a bug, filed as https://bugs.openjdk.java.net/browse/JDK-8096787, and fixed in version 8u60 which is expected to be released in August 2015.

How are two items being added to this observable list?

I'm coming from a Swing background and trying to pick up JavaFx.
This ObservableList is being filled with strings, and added to a ListView.
When I add an item to the observable list in the same thread, everything works fine.
However, when I try to add an item to the observable list from a different thread, the items are being added twice. For the life of me, I cannot figure out why. Debug statements show the Thread is in fact only executing once.
Here's a fully working example:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Callback;
public class FeedPanelViewer extends Application {
public static void main(String[] args) {
launch(args);
}
String greeting = "<html><body><p><strong>hi ya'll</strong></p></body></html>";
#Override
public void start(Stage stage) {
ObservableList<String> names = FXCollections.observableArrayList("Matthew", "Hannah", "Stephan", "Denise");
ListView<String> listView = new ListView<String>(names);
stage.setScene(new Scene(listView));
stage.show();
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> list) {
return new HtmlFormatCell();
}
});
// This thread is definitely only adding items once
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Platform.runLater(() -> {
System.out.println("Got here");
names.add(greeting);
names.add("andrew");
});
}).start();
}
public class HtmlFormatCell extends ListCell<String> {
public HtmlFormatCell() {
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
if (item.contains("<p>")) {
Platform.runLater(() -> {
WebView web = new WebView();
WebEngine engine = web.getEngine();
engine.loadContent(item);
web.setPrefHeight(50);
web.setPrefWidth(300);
web.autosize();
setText("");
setGraphic(web);
});
} else {
setText(item == null ? "" : "-" + item);
setTextFill(Color.BLUE);
if (isSelected()) {
setTextFill(Color.GREEN);
}
}
}
}
}
}
If I comment out the two lines new Thread(() -> { and }).start();, this is what I see:
And with the Thread wrapped around the addition of the two list elements, I'm seeing this, which is rendering the cells twice, even though the thread is only executing once:
Can anyone help point out what is going on?
Thanks very much.
In updateItem, you should have an else branch for when the item is empty (i.e. when item == null, or, better yet, when empty is true) and clear the cell, i.e. setText(null); setGraphic(null);.
Your updateItem method should something like this
if(!empty) {
// populate the cell with graphic and/or text
} else {
setText(null);
setGraphic(null);
}
In your example, it is likely that the last two cells are empty, but have not been cleared.
Note 1: The way ListView allocates and populates cells is (seemingly) unpredictable and it can do a lot of redundant item updates.
Note 2: This does not by itself explain the difference in behavior you get with your two versions. My guess is that without the Thread wrapper, the call gets executed before the ListView's first layout pass, while with the Thread wrapper, it layouts the initial items and then updates the layout for the added items. This, together with the previous note, could explain the difference in the results.
Note 3: In updateItem, you don't have to wrap your calls in Platform.runLater, since updateItem is already executed on the JavaFX application thread.

TableView - Edit focused cell

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);
}
}

Categories