Is there any way to get the original index of the selected row in TableView before the TableView was sorted or filtered? If not, can I alternatively make my own TableRow object, get it when selected and have a method getOriginalRowIdex() ?
The code tableView.getSelectionModel().getSelectedIndex()); returns the selected index according to the sorted and filtered data, which makes matching the row indexes to the indexes in a list not possible.
tableView.setRowFactory(tv -> {
TableRow<ObservableList> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (!row.isEmpty())) {
label.setText(rowMessages.get(tableView.getSelectionModel().getSelectedIndex()));
System.out.println(tableView.getSelectionModel().getSelectedIndex());
}
});
return row;
});
FilteredList and SortedList are just wrappers around a regular ObservableList. Since you have to have the original list, just look up the index of the data in the row using list.indexOf().
public class FilteredTable extends Application {
public static void main(String[] args){launch(args);}
#Override
public void start(Stage stage) {
ObservableList<LineItem> items = FXCollections.observableArrayList();
for (int i = 0;i<10;i++){items.add(new LineItem(i+"'th", i));}
TableView tableView = new TableView();
FilteredList<LineItem> evens = new FilteredList<>(items, p->p.amountProperty().get()%2==0);
SortedList<LineItem> sorted = new SortedList<>(evens);
sorted.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sorted);
TableColumn<LineItem,String> descCol = new TableColumn<>("desc");
descCol.setCellValueFactory(new PropertyValueFactory<>("desc"));
TableColumn<LineItem, Double> amountCol = new TableColumn<>("amount");
amountCol.setCellValueFactory(new PropertyValueFactory<>("amount"));
Label label = new Label("click a row");
tableView.setRowFactory(tv -> {
TableRow<ObservableList> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (!row.isEmpty())) {
label.setText(tableView.getSelectionModel().getSelectedIndex()
+" <-tbl row, idx in items-> "
+items.indexOf(tableView.getSelectionModel().getSelectedItem()));
}
});
return row;
});
tableView.getColumns().addAll(descCol,amountCol);
stage.setScene(new Scene(new VBox(5,tableView,label),300,300));
stage.show();
}
public class LineItem {
private final StringProperty desc = new SimpleStringProperty();
private final IntegerProperty amount = new SimpleIntegerProperty();
public StringProperty descProperty() {return desc;}
public IntegerProperty amountProperty() {return amount;}
public LineItem(String dsc, int amt) {
desc.set(dsc); amount.set(amt);
}
}
}
Related
I have a problem with JavaFx.
I have a table view, which shows the Data I want.
Now I change the Data in the TableView.
My changeListener works, so far so good.
Now I want the rows of the table with changes in it to appear in a different color.
But I just can't find a way to get the specific row.
I tried to google the solution but all I can find is how to get a selected row.
But there won't be any user input. The data just refreshes.
Can you help me?
Probably I was just to stupid to find the right keywords.
I think of something like:
tableview.getRow(indexOfChangedRow).setStyle
The TableRow maintains a property called Item, which holds the data for the row. You need a RowFactory that binds the background to some value in your Table data. Generally, your Table data is going to have fields composed of ObservableValues, like Properties. So you end up with a Property (the Table data Model), which is composed of Properties. This means that you'll need a ChangeListener to manually reset the Binding on the Background property of the TableRow whenever the Item property is changed.
Here's an example:
public class TableBackground extends Application {
private ObservableList<TableModel> tableData = FXCollections.observableArrayList();
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(createContent()));
primaryStage.show();
}
private Parent createContent() {
BorderPane borderPane = new BorderPane();
borderPane.setCenter(createTable());
borderPane.setBottom(createAddBox());
return borderPane;
}
private Node createTable() {
TableView<TableModel> tableView = new TableView<>();
TableColumn<TableModel, String> firstColumn = new TableColumn<>("Field 1");
firstColumn.setCellValueFactory(p -> p.getValue().field1Property());
tableView.getColumns().add(firstColumn);
TableColumn<TableModel, String> secondColumn = new TableColumn<>("Field 2");
secondColumn.setCellValueFactory(p -> p.getValue().field2Property());
tableView.getColumns().add(secondColumn);
tableView.setItems(tableData);
Background redBackground = new Background(new BackgroundFill(Color.RED, null, null));
Background blueBackground = new Background(new BackgroundFill(Color.CORNFLOWERBLUE, null, null));
tableView.setRowFactory(t -> {
TableRow<TableModel> row = new TableRow<TableModel>();
row.itemProperty().addListener((ob, oldValue, newValue) -> {
row.backgroundProperty().bind(Bindings.createObjectBinding(() -> newValue.isNewRow() ? redBackground : blueBackground,
newValue.newRowProperty()));
});
return row;
});
return tableView;
}
private Node createAddBox() {
TextField textField1 = new TextField();
TextField textField2 = new TextField();
Button button = new Button("Add Row");
button.setOnAction(evt -> {
tableData.forEach(dataItem -> dataItem.setNewRow(false));
tableData.add(new TableModel(textField1.getText(), textField2.getText()));
});
return new HBox(10, textField1, textField2, button);
}
}
And the TableModel would look like this:
public class TableModel {
private StringProperty field1 = new SimpleStringProperty("");
private StringProperty field2 = new SimpleStringProperty("");
private BooleanProperty newRow = new SimpleBooleanProperty(true);
public TableModel(String field1Data, String field2Data) {
field1.set(field1Data);
field2.set(field2Data);
setNewRow(true);
}
public String getField1() {
return field1.get();
}
public StringProperty field1Property() {
return field1;
}
public void setField1(String field1) {
this.field1.set(field1);
}
public String getField2() {
return field2.get();
}
public StringProperty field2Property() {
return field2;
}
public void setField2(String field2) {
this.field2.set(field2);
}
public boolean isNewRow() {
return newRow.get();
}
public BooleanProperty newRowProperty() {
return newRow;
}
public void setNewRow(boolean newRow) {
this.newRow.set(newRow);
}
}
The important part is the RowFactory:
Background redBackground = new Background(new BackgroundFill(Color.RED, null, null));
Background blueBackground = new Background(new BackgroundFill(Color.CORNFLOWERBLUE, null, null));
tableView.setRowFactory(t -> {
TableRow<TableModel> row = new TableRow<TableModel>();
row.itemProperty().addListener((ob, oldValue, newValue) -> {
row.backgroundProperty().bind(Bindings.createObjectBinding(() -> newValue.isNewRow() ? redBackground : blueBackground,
newValue.newRowProperty()));
});
return row;
});
It's a plain vanilla row except that it has a ChangeListener on row.itemProperty() which puts a Binding on row.backgroundProperty() based on value of new items TableModel.newRowProperty(). If it's new, then the background is red, otherwise it's blue.
The Button OnAction event, sets all of the existing rows newRowProperty() to false, and then adds a new row with newRowProperty() set to true. This means that all of the existing rows will turn blue, and the new row will be red.
I'm Actually new with JavaFX and been facing an issue to view the data on the table.
What I'm trying to do is to create a new stage on a Mouse Click Event of an Imageview where the Fields will be provided to the user to enter the data to the table on the parent node.
This is the function which is called on the mouse click event that creates a child node(stage) :
private void addQualification(MouseEvent event) {
System.out.println("Add Qualification Method");
final Stage dialog = new Stage();
dialog.setTitle("Add a Qualification");
dialog.initModality(Modality.WINDOW_MODAL);
dialog.initStyle(StageStyle.UTILITY);
// create a grid for the data entry.
GridPane grid = new GridPane();
AnchorPane anchorPane = new AnchorPane(grid);
ObservableList<String> QualChoiceList = FXCollections.observableArrayList("MBBS/MSC ", "MD/MS/DNB/PhD", "DM/M Ch.");
final ChoiceBox<String> QualChoice = new ChoiceBox<String>();
QualChoice.setValue("Select a Qualification");
QualChoice.setItems(QualChoiceList);
final TextField College = new TextField();
final TextField University = new TextField();
final TextField PassingYear = new TextField();
final TextField RegNo = new TextField();
final TextField StateName = new TextField();
grid.addRow(0, new Label("Qualification"), QualChoice);
grid.addRow(1, new Label("College"), College);
grid.addRow(2, new Label("University"), University);
grid.addRow(3, new Label("Year of Passing"), PassingYear);
grid.addRow(4, new Label("Registration No."), RegNo);
grid.addRow(5, new Label("Name of State"), StateName);
grid.setHgap(10);
grid.setVgap(10);
GridPane.setHgrow(QualChoice, Priority.ALWAYS);
GridPane.setHgrow(College, Priority.ALWAYS);
GridPane.setHgrow(University, Priority.ALWAYS);
GridPane.setHgrow(PassingYear, Priority.ALWAYS);
GridPane.setHgrow(RegNo, Priority.ALWAYS);
GridPane.setHgrow(StateName, Priority.ALWAYS);
// create action buttons for the dialog.
Button ok = new Button("Add");
Button cancel = new Button("Cancel");
ok.setDefaultButton(true);
cancel.setCancelButton(true);
anchorPane.getChildren().add(ok);
anchorPane.getChildren().add(cancel);
AnchorPane.setTopAnchor(grid, 20.0);
AnchorPane.setLeftAnchor(grid, 20.0);
AnchorPane.setRightAnchor(grid, 20.0);
AnchorPane.setBottomAnchor(grid, 80.0);
AnchorPane.setTopAnchor(ok, 240.0);
AnchorPane.setLeftAnchor(ok, 100.0);
AnchorPane.setRightAnchor(ok, 214.0);
AnchorPane.setBottomAnchor(ok, 30.0);
AnchorPane.setTopAnchor(cancel, 240.0);
AnchorPane.setLeftAnchor(cancel, 230.0);
AnchorPane.setRightAnchor(cancel, 95.0);
AnchorPane.setBottomAnchor(cancel, 30.0);
dialog.setScene(new Scene(anchorPane, 400, 310));
dialog.show();
// only enable the ok button when there has been some text entered.
ok.disableProperty().bind(College.textProperty().isEqualTo("").or(University.textProperty().isEqualTo("")).or(PassingYear.textProperty().isEqualTo("")).or(RegNo.textProperty().isEqualTo("")).or(StateName.textProperty().isEqualTo("")));
// add action handlers for the dialog buttons.
ok.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
int nextIndex = QualTable.getSelectionModel().getSelectedIndex() + 1;
QualTable.getItems().add(nextIndex, new Qualification(QualChoice.getValue(), College.getText(), University.getText(), PassingYear.getText(), RegNo.getText(), StateName.getText()));
QualTable.getSelectionModel().select(nextIndex);
dialog.close();
}
});
cancel.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
dialog.close();
}
});
This is the Qualification Class that I have used to inout the values to the QualTable using the add Method:
public static class Qualification {
private StringProperty Qual;
private StringProperty College;
private StringProperty University;
private StringProperty PassingYear;
private StringProperty RegNo;
private StringProperty StateName;
public Qualification(String Qual, String College, String University, String PassingYear, String RegNo, String StateName) {
setQual(Qual);
setCollege(College);
setUniversity(University);
setPassingYear(PassingYear);
setRegNo(RegNo);
setStateName(StateName);
}
public String getQual() {
return QualProperty().get();
}
public String getCollege() {
return CollegeProperty().get();
}
public String getUniversity() {
return UniversityProperty().get();
}
public String getPassingYear() {
return PassingYearProperty().get();
}
public String getRegNo() {
return RegNoProperty().get();
}
public String getStateName() {
return StateNameProperty().get();
}
public final void setQual(String value) {
QualProperty().set(value);
}
public final void setCollege(String Coll) {
CollegeProperty().set(Coll);
}
public final void setUniversity(String Univ) {
UniversityProperty().set(Univ);
}
public final void setPassingYear(String PsngYr) {
PassingYearProperty().set(PsngYr);
}
public final void setRegNo(String Reg) {
RegNoProperty().set(Reg);
}
public final void setStateName(String StNm) {
StateNameProperty().set(StNm);
}
public StringProperty QualProperty() {
if (Qual == null) {
Qual = new SimpleStringProperty(this, "Qual");
}
return Qual;
}
public StringProperty CollegeProperty() {
if (College == null) {
College = new SimpleStringProperty(this, "College");
}
return College;
}
public StringProperty UniversityProperty() {
if (University == null) {
University = new SimpleStringProperty(this, "University");
}
return University;
}
public StringProperty PassingYearProperty() {
if (PassingYear == null) {
PassingYear = new SimpleStringProperty(this, "PassingYear");
}
return PassingYear;
}
public StringProperty RegNoProperty() {
if (RegNo == null) {
RegNo = new SimpleStringProperty(this, "RegNo");
}
return RegNo;
}
public StringProperty StateNameProperty() {
if (StateName == null) {
StateName = new SimpleStringProperty(this, "StateName");
}
return StateName;
}
}
This is the FXML code of the Anchor Pane consisting the tableview QualTable
I Need help on this tableview and the function that I'm using to populate the table view...
The values are properly carried through the Qualification class but the tableview is not displaying the values on the row....
Please help me out through this and Thanks in Advance..!!!
I have a TableView with a on column contains a article and an other column with a price. I would like to change the row's css of a index row when I click in a button.
I have this :
articleTable.setRowFactory(param -> new TableRow<LigneTicket>() {
#Override
protected void updateItem(LigneTicket paramT, boolean empty) {
super.updateItem(paramT, empty);
if (!isEmpty() && paramT != null && paramT.getArticle().isArticleAnnuler()) {
getStyleClass().add("articleCanceled");
articleTable.refresh();
}
}
});
But this code work on change of my tableView, not just on the click in a button, and it's not working in a index row choose.
Help please,
Thanks
Try use one hack after your event:
for ( Column col : articleTable.getColumns() ) {
col.setVisible( false );
col.setVisible( true );
}
Note that there is not a 1:1 relation between table items and table rows.
TableRows only exist for visible items and may be reassigned a value. You therefore need to take care properly removing the style class.
Furthermore instead of using a style class to mark the rows, I recommend using a pseudoclass, which allows easier adding/removing.
You could store the data about the state of the item in the item class itself or store it in a suitable external datastructre.
The following example adds/removes the removed pseudoclass to/from table rows when the value associated with the item in a ObservableMap changes.
The buttons allow assigning or clearing the pseudoclass from the selected rows.
You could do a similar thing with indices instead of items too.
#Override
public void start(Stage primaryStage) {
TableView<Item> tableView = new TableView<>(FXCollections.observableArrayList(
new Item("a"),
new Item("b"),
new Item("c"),
new Item("d"),
new Item("e"),
new Item("f")
));
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
ObservableMap<Item, Boolean> removed = FXCollections.observableHashMap();
PseudoClass removedPseudoClass = PseudoClass.getPseudoClass("removed");
tableView.setRowFactory(tv -> {
TableRow<Item> result = new TableRow<>();
ObjectBinding<Boolean> binding = Bindings.valueAt(removed, result.itemProperty());
binding.addListener((observable, oldValue, newValue) -> result.pseudoClassStateChanged(removedPseudoClass, newValue != null && newValue));
return result;
});
TableColumn<Item, String> column = new TableColumn<>("value");
column.setCellValueFactory(td -> td.getValue().valueProperty());
tableView.getColumns().add(column);
Button btn = new Button("remove");
Button btn2 = new Button("add");
btn.setOnAction(evt -> {
for (Item item : tableView.getSelectionModel().getSelectedItems()) {
removed.put(item, Boolean.TRUE);
}
});
btn2.setOnAction(evt -> {
for (Item item : tableView.getSelectionModel().getSelectedItems()) {
removed.remove(item);
}
});
Scene scene = new Scene(new VBox(10, tableView, btn, btn2));
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public class Item {
public Item() {
}
public Item(String value) {
this.value.set(value);
}
private final StringProperty value = new SimpleStringProperty();
public String getValue() {
return value.get();
}
public void setValue(String val) {
value.set(val);
}
public StringProperty valueProperty() {
return value;
}
}
style.css
.table-row-cell:filled {
-fx-background-color: lime;
}
.table-row-cell:filled:selected {
-fx-background-color: -fx-selection-bar;
}
.table-row-cell:filled:removed {
-fx-background-color: orange;
}
.table-row-cell:filled:removed:selected {
-fx-background-color: -fx-selection-bar;
}
So I have code that works if the user selects a different row than the one currently selected
table.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue == null) {
updateDetails(oldValue);
return;
}
updateDetails(newValue);
});
}
However, I want this to work if the user clicks on the same value as well - basically, there's a part of the code that modifies an image shown but that image doesn't update itself unless I click on another row then go back to the row I was previously on. I would like to be able to update the row I'm on simply by clicking on it (which would call updateDetails) but can't seem to figure this out...
Create a custom rowFactory and add a mouse listener to it.
Example
This displays the old value property of the last item clicked and the new item clicked as text of the Label.
TableView<Item> tv = new TableView<>(FXCollections.observableArrayList(new Item("foo"), new Item("bar"), new Item("42")));
Label label = new Label();
TableColumn<Item, String> valueColumn = new TableColumn<>("value");
valueColumn.setCellValueFactory(d -> d.getValue().valueProperty());
tv.getColumns().add(valueColumn);
EventHandler<MouseEvent> eventHandler = new EventHandler<MouseEvent>() {
private Item lastItem;
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
TableRow<Item> source = (TableRow<Item>) event.getSource();
if (!source.isEmpty()) {
label.setText(MessageFormat.format("old: {0}; new: {1}", lastItem == null ? null : lastItem.getValue(), (lastItem = source.getItem()).getValue()));
}
}
}
};
tv.setRowFactory(t -> {
TableRow<Item> row = new TableRow();
row.setOnMouseClicked(eventHandler);
return row;
});
public class Item {
public Item() {
}
public Item(String value) {
this.value.set(value);
}
private final StringProperty value = new SimpleStringProperty();
public String getValue() {
return value.get();
}
public void setValue(String val) {
value.set(val);
}
public StringProperty valueProperty() {
return value;
}
}
Problem
I'd like to switch to edit mode in my TableView as soon as I type. I don't want to doubleclick or press to enter on each and every cell first, that's annoying.
I've come up with the following piece of code. Problem is that it is more or less side-effect programming and I suspect troubles. When you use KEY_RELEASED in order to switch the table into edit mode, the 1st key press gets lost.
So you have to use KEY_PRESSED. It all seems to work fine now, but once in a while you get a race condition and the caret in the TextField cell editor is before the typed text instead of after it. But when you continue typing, then the text gets appended correctly after the existing text.
It appears okay, but from a developing point of view it seems like a mess with race conditions.
Question
Does anyone have a proper way of doing a "type-to-edit" functionality?
Code
Here's the code I've got so far:
public class InlineEditingTableView extends Application {
private final ObservableList<Data> data =
FXCollections.observableArrayList(
new Data(1.,5.),
new Data(2.,6.),
new Data(3.,7.),
new Data(4.,8.)
);
private TableView<Data> table;
#Override
public void start(Stage stage) {
// create edtiable table
table = new TableView<Data>();
table.setEditable(true);
// column 1 contains numbers
TableColumn<Data, Number> number1Col = new TableColumn<>("Number 1");
number1Col.setMinWidth(100);
number1Col.setCellValueFactory( cellData -> cellData.getValue().number1Property());
number1Col.setCellFactory( createNumberCellFactory());
number1Col.setOnEditCommit(new EventHandler<CellEditEvent<Data, Number>>() {
#Override
public void handle(CellEditEvent<Data, Number> t) {
System.out.println( t);
// ((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
}
});
// column 2 contains numbers
TableColumn<Data, Number> number2Col = new TableColumn<>("Number 2");
number2Col.setMinWidth(100);
number2Col.setCellValueFactory( cellData -> cellData.getValue().number2Property());
number2Col.setCellFactory( createNumberCellFactory());
// add columns & data to table
table.setItems(data);
table.getColumns().addAll( number1Col, number2Col);
// switch to edit mode on keypress
// this must be KeyEvent.KEY_PRESSED so that the key gets forwarded to the editing cell; it wouldn't be forwarded on KEY_RELEASED
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
// event.consume(); // don't consume the event or else the values won't be updated;
return;
}
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null) {
if( event.getCode().isLetterKey() || event.getCode().isDigitKey()) {
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
}
}
}
});
table.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
table.getSelectionModel().selectBelowCell();
}
}
});
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
// add nodes to stage
BorderPane root = new BorderPane();
root.setCenter(table);
Scene scene = new Scene( root, 800,600);
stage.setScene(scene);
stage.show();
}
/**
* Number cell factory which converts strings to numbers and vice versa.
* #return
*/
private Callback<TableColumn<Data, Number>, TableCell<Data, Number>> createNumberCellFactory() {
Callback<TableColumn<Data, Number>, TableCell<Data, Number>> factory = TextFieldTableCell.forTableColumn( new StringConverter<Number>() {
#Override
public Number fromString(String string) {
return Double.parseDouble(string);
}
#Override
public String toString(Number object) {
return object.toString();
}
});
return factory;
}
/**
* Table data container
*/
public static class Data {
private final SimpleDoubleProperty number1;
private final SimpleDoubleProperty number2;
private Data( Double number1, Double number2) {
this.number1 = new SimpleDoubleProperty(number1);
this.number2 = new SimpleDoubleProperty(number2);
}
public final DoubleProperty number1Property() {
return this.number1;
}
public final double getNumber1() {
return this.number1Property().get();
}
public final void setNumber1(final double number1) {
this.number1Property().set(number1);
}
public final DoubleProperty number2Property() {
return this.number2;
}
public final double getNumber2() {
return this.number2Property().get();
}
public final void setNumber2(final double number2) {
this.number2Property().set(number2);
}
}
public static void main(String[] args) {
launch(args);
}
}
To edit immediately on clicking a cell, it makes more sense to me to have the TextFields permanently displayed in the table, instead of transitioning to a special "edit mode" and switch from a Label to a TextField. (I would think of this as having all cells always in "edit mode", which I think makes sense with the behavior you want.)
If that kind of UI works for your requirements, you can just render text fields in the cell and bind bidirectionally the text field's textProperty to the appropriate property in your model. The tricky part here is getting hold of that property: you have to go from the cell to the table row, then to the item for the table row, and then to the property you need. At any time, one of those may change (possibly to null), so you have to deal with those possibilities.
Give the usual example:
public class Person {
// ...
public StringProperty firstNameProperty() { ... }
// etc...
}
You can do
TableView<Person> table = new TableView<>();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
textField.textProperty().unbindBidirectional(((Person) oldPerson).firstNameProperty());
}
if (newPerson != null) {
textField.textProperty().bindBidirectional(((Person) newPerson).firstNameProperty());
}
};
cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
if (oldRow != null) {
oldRow.itemProperty().removeListener(rowItemListener);
if (oldRow.getItem() != null) {
textField.textProperty().unbindBidirectional(((Person) oldRow.getItem()).firstNameProperty());
}
}
if (newRow != null) {
newRow.itemProperty().addListener(rowItemListener);
if (newRow.getItem() != null) {
textField.textProperty().bindBidirectional(((Person) newRow.getItem()).firstNameProperty());
}
}
});
return cell ;
});
You can greatly reduce the code complexity here by using the EasyBind framework, which provides (among other things) ways to get "properties of properties" with appropriate handling for null:
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
textField.textProperty().bindBidirectional(
EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> ((Person)p).firstNameProperty()));
return cell ;
});
Here is a complete example, where I factored the cell factory code above into a more general method:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.easybind.EasyBind;
public class LiveTableViewCell extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
table.getColumns().addAll(
createColumn("First Name", Person::firstNameProperty),
createColumn("Last Name", Person::lastNameProperty),
createColumn("Email", Person::emailProperty)
);
Button button = new Button("Debug");
button.setOnAction(e -> table.getItems().stream().map(p -> String.format("%s %s %s", p.getFirstName(), p.getLastName(), p.getEmail())).forEach(System.out::println));
primaryStage.setScene(new Scene(new BorderPane(table, null, null, button, null), 600, 120));
primaryStage.show();
}
private TableColumn<Person, String> createColumn(String title, Function<Person, Property<String>> property) {
TableColumn<Person, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
// Example of maintaining selection behavior when text field gains
// focus. You can also call getSelectedCells().add(...) on the selection
// model if you want to maintain multiple selected cells, etc.
textField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
if (isFocused) {
cell.getTableView().getSelectionModel().select(cell.getIndex(), cell.getTableColumn());
}
});
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
// If not using EasyBind, you need the following commented-out code in place of the next statement:
// ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
// if (oldPerson != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldPerson));
// }
// if (newPerson != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newPerson));
// }
// };
// cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
// if (oldRow != null) {
// oldRow.itemProperty().removeListener(rowItemListener);
// if (oldRow.getItem() != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldRow.getItem()));
// }
// }
// if (newRow != null) {
// newRow.itemProperty().addListener(rowItemListener);
// if (newRow.getItem() != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newRow.getItem()));
// }
// }
// });
textField.textProperty().bindBidirectional(EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> (property.apply((Person)p))));
return cell ;
});
return col ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
(The annoying downcasts here are because TableCell<S,T>.getTableRow() returns a raw TableRow object, instead of a TableRow<S>, for reasons I have never understood.)
I think you can avoid it by implementing custom text field tablecell, where you can put the caret at the end of the item text manually on entering edit mode.
Another approach is to enter edit mode on focus:
table.getFocusModel().focusedCellProperty().addListener(
( ObservableValue<? extends TablePosition> observable, TablePosition oldValue, TablePosition newValue ) ->
{
if ( newValue != null )
{
Platform.runLater( () ->
{
table.edit( newValue.getRow(), newValue.getTableColumn() );
} );
}
}
);
a couple of years late, but I actually found a solution to this (using a Robot).
this.setOnKeyTyped(x -> {
String typed = x.getCharacter();
//can make editing start only when certain keys (e.g. digits) are typed.
if(typed != null && typed.matches("[0-9]")) {
Robot robot = new Robot();
robot.keyPress(KeyCode.ENTER);
}
});