I am trying to make a TableColumn that displays Message title and message body. It should have behavior similar to Gmail. That is display Title (bold), and Message body (regular) in one line.
Message title Message body. Both strings are short - can display all
Message title Now Message body is long. Only display the beginning of ...
Now message title is quite long too. Only message title fits this time...
Problem
Label, Text: cannot display two styles.
HBox with two Text : display ...ge Ti... ..ssage Body... (desired: Message Title Me..)
TextFlow, WebView: cannot force setWrap(false). JavaFX displays ugly multiline text.
Compute subString based on ColumnWidth using code below. Fires recalculation of graphics of all cells on each column resize. Possible but feels like reinventing a wheel for a low level language.
Is there a build in way to display the beginning of a string with two styles in one line?
private Double getSubstringLength(String preparedTitle, String headerStyle){
Text disposable = new Text();
disposable.setText(preparedTitle);
disposable.setStyle(headerStyle);
return disposable.getBoundsInLocal().getWidth();
}
You can use a custom layout pane.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class MailViewSample extends Application {
private final ObservableList<Message> data =
FXCollections.observableArrayList(
new Message("Jacob", "Message title 1", "Message body. Both strings are short - can display all"),
new Message("Isabella", "Message title 2", "Now Message body is long. Only display the beginning of this longer body"),
new Message("Ethan", "Now message title is quite long too. Only message title fits this time no body seen", "Message body not seen")
);
public static void main(String[] args) {
launch(args);
}
class MessagePane extends Pane {
private static final double SPACING = 10;
private final Label title;
private final Label body;
public MessagePane(Label title, Label body) {
super(title, body);
this.title = title;
this.body = body;
}
#Override
protected void layoutChildren() {
double width = getWidth();
double height = getHeight();
double baselineOffset = Math.max(title.getBaselineOffset(), body.getBaselineOffset());
if (title.prefWidth(-1) + SPACING + body.minWidth(-1) > width) {
layoutInArea(title, 0, 0, width, height, baselineOffset, HPos.LEFT, VPos.BASELINE);
layoutInArea(body, 0, 0, 0, 0, baselineOffset, HPos.LEFT, VPos.BASELINE);
} else {
double titleWidth = title.prefWidth(-1);
layoutInArea(title, 0, 0, titleWidth, height, baselineOffset, HPos.LEFT, VPos.BASELINE);
double bodyWidth = Math.max(0, Math.min(width - (titleWidth + SPACING), body.prefWidth(-1)));
layoutInArea(body, titleWidth + SPACING, 0, bodyWidth, height, baselineOffset, HPos.LEFT, VPos.BASELINE);
}
}
}
#Override
public void start(Stage stage) {
TableView<Message> table = new TableView<>(data);
table.setPrefHeight(150);
TableColumn<Message, String> senderCol = new TableColumn<>("Sender");
senderCol.setCellValueFactory(new PropertyValueFactory<>("sender"));
table.getColumns().add(senderCol);
TableColumn<Message, Message> messageCol = new TableColumn<>("Message");
messageCol.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue()));
messageCol.setCellFactory(param -> new TableCell<Message, Message>() {
Label title = new Label();
Label body = new Label();
MessagePane messagePane = new MessagePane(title, body);
{
title.setStyle("-fx-font-weight: bold;");
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
title.textProperty().unbind();
body.textProperty().unbind();
if (empty || item == null) {
setGraphic(null);
} else {
title.setText(item.getTitle());
body.setText(item.getBody());
title.textProperty().bind(item.titleProperty());
body.textProperty().bind(item.bodyProperty());
setGraphic(messagePane);
}
}
});
messageCol.setPrefWidth(520);
table.getColumns().add(messageCol);
stage.setScene(new Scene(table));
stage.show();
}
public static class Message {
private final SimpleStringProperty sender;
private final SimpleStringProperty title;
private final SimpleStringProperty body;
private Message(String sender, String title, String body) {
this.sender = new SimpleStringProperty(sender);
this.title = new SimpleStringProperty(title);
this.body = new SimpleStringProperty(body);
}
public SimpleStringProperty senderProperty() {
return sender;
}
public String getSender() {
return sender.get();
}
public void setSender(String sender) {
this.sender.set(sender);
}
public SimpleStringProperty titleProperty() {
return title;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public SimpleStringProperty bodyProperty() {
return body;
}
public String getBody() {
return body.get();
}
public void setBody(String body) {
this.body.set(body);
}
}
}
Related
I'm currently trying to implement the following:
A TableView with an ObservableList as dataset, with two columns, each of which contains Strings (names of the players). This part is easy enough.
Once a Player(name) is clicked, a custom FlowPane should be injected below the selected player. If another player is clicked, the flowpane should disappear and be injected below the currently clicked player.
The below code implements the TableView (minus the mouse listener part). Please help me let the FlowPane span the entire row. I'm guessing I need a RowFactory but have no clue how to make it work for my purposes :)
Also, apparently both my columns now show the same data. Confusing :) Is there a way to tell one column to use half the data set and the other column the other half? I obviously don't want my data shown twice.
public class main extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage) throws Exception
{
try
{
FlowPane f = new FlowPane();
Scene scene = new Scene(f, 300, 200);
Player p1 = new Player("player 1 ");
Player p2 = new Player("player 2 ");
Player p3 = new Player("player 3 ");
ArrayList<Object> players = new ArrayList<>();
players.add(p1);
players.add(p2);
players.add(p3);
ObservableList<Object> observableList = FXCollections.observableArrayList(players);
TableView<Object> table = createTableView(observableList, 300, 200);
f.getChildren().add(table);
injectFlowPane(table);
stage.setScene(scene);
stage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public TableView<Object> createTableView(ObservableList<Object> items, double width, double height)
{
TableView<Object> table = new TableView<>();
table.setItems(items);
table.getColumns().add(createTableColumn(width / 2));
table.getColumns().add(createTableColumn(width / 2));
table.setMinSize(width, height);
table.setPrefSize(width, height);
table.setMaxSize(width, height);
return table;
}
private TableColumn<Object, Object> createTableColumn(double width)
{
TableColumn<Object, Object> tableColumn = new TableColumn<>();
tableColumn.setCellFactory(
new Callback<TableColumn<Object, Object>, TableCell<Object, Object>>() {
#Override
public TableCell<Object, Object> call(TableColumn<Object, Object> arg0)
{
return new PlayerCell();
}
});
tableColumn.setCellValueFactory(cellDataFeatures -> {
Object item = cellDataFeatures.getValue();
return new SimpleObjectProperty<>(item);
});
tableColumn.setMinWidth(width);
return tableColumn;
}
private void injectFlowPane(TableView<Object> table)
{
FlowPane f = new FlowPane();
f.setMinSize(50, 50);
f.setBackground(new Background(new BackgroundFill(Color.DARKGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
table.getItems().add(1, f);
}
}
public class PlayerCell extends TableCell<Object, Object>
{
#Override
protected void updateItem(Object item, boolean empty)
{
super.updateItem(item, false);
// if (empty)
if (item != null)
{
if (item instanceof Player)
{
setText(((Player) item).getName());
setGraphic(null);
}
else if (item instanceof FlowPane)
{
setGraphic((FlowPane) item);
}
else
{
setText("N/A");
setGraphic(null);
}
}
else
{
setText(null);
setGraphic(null);
}
}
}
public class Player
{
private String name;
public Player(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
EDIT:
I have now implemented James_D's ExpandingTableRow, which works neatly as far as showing the FlowPane below the selected TableRow is concerned. I have also managed to change my datastructures so that each column now shows different players instead of the same ones in each column.
However, the FlowPane that is created should actually depend on the actual player(cell) that is clicked within the row. In James' example: a different FlowPane would be created if the FirstName or LastName was selected (even for the same row). The FlowPane should be shown the same way - below the selected row - but it's a different, new FlowPane depending on if FirstName was clicked, or if LastName was clicked. How can I manage to do this?
I've looked at using:
table.getSelectionModel().setCellSelectionEnabled(true);
But this actually seems to disable James_d's solution.
This solution works only in Java 9 and later.
The display of a row is managed by a TableRow, and the actual layout of that row is performed by its skin (a TableRowSkin). So to manage this, you need a subclass of TableRow that installs a custom skin.
The row implementation is pretty straightforward: in this example I added a property for the "additional content" to be displayed when the row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TableRow;
public class ExpandingTableRow<T> extends TableRow<T> {
private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>();
public final ObjectProperty<Node> selectedRowContentProperty() {
return this.selectedRowContent;
}
public final Node getSelectedRowContent() {
return this.selectedRowContentProperty().get();
}
public final void setSelectedRowContent(final Node selectedRowContent) {
this.selectedRowContentProperty().set(selectedRowContent);
}
public ExpandingTableRow(Node selectedRowContent) {
super();
setSelectedRowContent(selectedRowContent);
}
public ExpandingTableRow() {
this(null);
}
#Override
protected Skin<?> createDefaultSkin() {
return new ExpandingTableRowSkin<T>(this);
}
}
The skin implementation has to do the layout work. It needs to override the methods that compute the height, accounting for the height of the extra content if needed, and it needs to override the layoutChildren() method, to position the additional content, if needed. Finally, it must manage the additional content, adding or removing the additional content if the selected state of the row changes (or if the additional content itself is changed).
import javafx.scene.control.skin.TableRowSkin;
public class ExpandingTableRowSkin<T> extends TableRowSkin<T> {
private ExpandingTableRow<T> row;
public ExpandingTableRowSkin(ExpandingTableRow<T> row) {
super(row);
this.row = row;
row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> {
if (oldContent != null) {
getChildren().remove(oldContent);
}
if (newContent != null && row.isSelected()) {
getChildren().add(newContent);
}
if (row.getTableView() != null) {
row.getTableView().requestLayout();
}
});
row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected && row.getSelectedRowContent() != null
&& !getChildren().contains(row.getSelectedRowContent())) {
getChildren().add(row.getSelectedRowContent());
} else {
getChildren().remove(row.getSelectedRowContent());
}
});
}
#Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().maxHeight(width);
}
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().minHeight(width);
}
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().prefHeight(width);
}
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected void layoutChildren(double x, double y, double w, double h) {
if (row.isSelected()) {
double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(),
snappedLeftInset());
super.layoutChildren(x, y, w, rowHeight);
row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight);
} else {
super.layoutChildren(x, y, w, h);
}
}
}
Finally, a test (using the usual example from Oracle, or a version of it):
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class ExpandingTableRowTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getColumns().add(column("First Name", Person::firstNameProperty));
table.getColumns().add(column("Last Name", Person::lastNameProperty));
table.setRowFactory(tv -> {
Label label = new Label();
FlowPane flowPane = new FlowPane(label);
TableRow<Person> row = new ExpandingTableRow<>(flowPane) {
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
label.setText(null);
} else {
label.setText(String.format("Some additional information about %s %s here",
person.getFirstName(), person.getLastName()));
}
}
};
return row;
});
table.getItems().addAll(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S, T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
setFirstName(firstName);
setLastName(lastName);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final String lastName) {
this.lastNameProperty().set(lastName);
}
}
public static void main(String[] args) {
launch(args);
}
}
As you can see, a little refinement of the style and sizing may be needed to get this production-ready, but this shows the approach that will work.
I would like to add multiple combo boxes to JavaFX that after the user has selected an item the cost of that item will be displayed under the combo box. Also that the total cost of all the selected items will be displayed at the bottom. I know how to make one combo box that will display the cost of one item selected but can't figure out how to make multiple ones and to display the cost of everything selected
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.collections.FXCollections;
public class Animals extends Application {
Stage window;
Scene scene;
Button button;
ComboBox<Animal> comboBox = new ComboBox<Animal>();
Text textNamePrice = new Text();
static public TextField[] tfLetters = new TextField[37];
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
window = primaryStage;
window.setTitle("ComboBox ");
button = new Button("Submit");
comboBox = new ComboBox<Animal>();
comboBox.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object.getName();
}
#Override
public Animal fromString(String string) {
return null;
}
});
comboBox.setItems(FXCollections.observableArrayList(new Animal("Dog", 30.12), new Animal("Cat", 23.23),
new Animal("Bird", 15.0)));
comboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
String selectionText = "Price of the " + newVal.getName() + " is : " + newVal.getPrice();
System.out.println(selectionText);
textNamePrice.setText(selectionText);
});
VBox layout = new VBox(10);
layout.setPadding(new Insets(60, 60, 60, 60));
layout.getChildren().addAll(comboBox, textNamePrice, button);
scene = new Scene(layout, 500, 350);
window.setScene(scene);
window.show();
}
public class Animal {
private String name;
private Double price;
public Double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, Double price) {
this.name = name;
this.price = price;
}
}
}
It's probabls easiest to use a custom Node type AnimalChooser for displaying the ComboBox + price. This way the functionality for one selection+price display can be handled in one place. Also you can provide a price property based on the selection to sum them up from you application class.
The following example places all those AnimalChoosers in an VBox and adds a listener to the child list to add and remove listeners to/from the child list, should it be modified, which would allow you to dynamically add/remove those AnimalChooser to/from the VBox and still get a properly updated sum.
public class Animal {
private final String name;
// primitive type should be prefered here
private final double price;
public double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, double price) {
this.name = name;
this.price = price;
}
}
public class AnimalChooser extends VBox {
private final ComboBox<Animal> animalCombo;
private final ReadOnlyDoubleWrapper price;
private final Text text;
public AnimalChooser(ObservableList<Animal> items) {
setSpacing(5);
animalCombo = new ComboBox<>(items);
// converter for using a custom string representation of Animal in the
// combobox
animalCombo.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object == null ? "" : object.getName();
}
#Override
public Animal fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
} else {
// find suitable animal from list
Animal animal = null;
for (Animal item : items) {
if (string.equals(item.getName())) {
animal = item;
break;
}
}
return animal;
}
}
});
text = new Text();
price = new ReadOnlyDoubleWrapper();
getChildren().addAll(animalCombo, text);
// bind price value to price property
price.bind(Bindings.createDoubleBinding(new Callable<Double>() {
#Override
public Double call() throws Exception {
Animal animal = animalCombo.getValue();
return animal == null ? 0d : animal.getPrice();
}
}, animalCombo.valueProperty()));
// bind text to content of Text node
text.textProperty().bind(Bindings.when(animalCombo.valueProperty().isNull()).then("").otherwise(price.asString("%.2f $")));
}
public final double getPrice() {
return this.price.get();
}
public final ReadOnlyDoubleProperty priceProperty() {
return this.price.getReadOnlyProperty();
}
}
#Override
public void start(Stage primaryStage) {
VBox animalChoosers = new VBox(20);
ObservableList<Animal> animals = FXCollections.observableArrayList(
new Animal("cat", 1000.99),
new Animal("dog", 20.50),
new Animal("goldfish", 15.22)
);
final DoubleProperty total = new SimpleDoubleProperty();
InvalidationListener listener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
double sum = 0d;
for (Node n : animalChoosers.getChildren()) {
AnimalChooser chooser = (AnimalChooser) n;
sum += chooser.getPrice();
}
total.set(sum);
}
};
// just in case you want to add AnimalChooser s dynamially to animalChoosers
animalChoosers.getChildren().addListener(new ListChangeListener<Node>() {
#Override
public void onChanged(ListChangeListener.Change<? extends Node> c) {
while (c.next()) {
// add remove listeners updating the total
for (Node n : c.getRemoved()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().removeListener(listener);
}
for (Node n : c.getAddedSubList()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().addListener(listener);
}
}
listener.invalidated(null);
}
});
for (int i = 0; i < 10; i++) {
animalChoosers.getChildren().add(new AnimalChooser(animals));
}
BorderPane root = new BorderPane(animalChoosers);
Text totalText = new Text();
totalText.textProperty().bind(total.asString("total: %.2f $"));
root.setBottom(totalText);
BorderPane.setMargin(totalText, new Insets(20));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
How do I get the name of the column of a textfield inside a javaFX table?
I need this to check the value of the cells only in the "text2" column. I tried it with textfield.parent() but I didn't get a useful result.Edit: I just removed some unnessary log, which was not helpful for understanding.Now it is more convenient.
Here is my Code:
import java.util.ArrayList;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextArea;
import javafx.util.Callback;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
/*interface inside_table
{
public String get_column_name
}*/
public class Supermain extends Application {
#Override
public void start(Stage primaryStage) {
ArrayList myindizes=new ArrayList();
final TableView<myTextRow> table = new TableView<>();
table.setEditable(true);
table.setStyle("-fx-text-wrap: true;");
//Table columns
TableColumn<myTextRow, String> clmID = new TableColumn<>("ID");
clmID.setMinWidth(160);
clmID.setCellValueFactory(new PropertyValueFactory<>("ID"));
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
clmtext.setCellFactory(new TextFieldCellFactory());
TableColumn<myTextRow, String> clmtext2 = new TableColumn<>("Text2");
clmtext2.setMinWidth(160);
clmtext2.setCellValueFactory(new PropertyValueFactory<>("text2"));
clmtext2.setCellFactory(new TextFieldCellFactory());
//Add data
final ObservableList<myTextRow> data = FXCollections.observableArrayList(
new myTextRow(5, "Lorem","bla"),
new myTextRow(2, "Ipsum","bla")
);
table.getColumns().addAll(clmID, clmtext,clmtext2);
table.setItems(data);
HBox hBox = new HBox();
hBox.setSpacing(5.0);
hBox.setPadding(new Insets(5, 5, 5, 5));
Button btn = new Button();
btn.setText("Get Data");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (myTextRow data1 : data) {
System.out.println("data:" + data1.getText2());
}
}
});
hBox.getChildren().add(btn);
BorderPane pane = new BorderPane();
pane.setTop(hBox);
pane.setCenter(table);
primaryStage.setScene(new Scene(pane, 640, 480));
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public static class TextFieldCellFactory
implements Callback<TableColumn<myTextRow, String>, TableCell<myTextRow, String>> {
#Override
public TableCell<myTextRow, String> call(TableColumn<myTextRow, String> param) {
TextFieldCell textFieldCell = new TextFieldCell();
return textFieldCell;
}
public static class TextFieldCell extends TableCell<myTextRow, String> {
private TextArea textField;
private StringProperty boundToCurrently = null;
private String last_text;
public TextFieldCell() {
textField = new TextArea();
textField.setWrapText(true);
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
last_text="";
this.setGraphic(textField);
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
//only if textfield is in the text2 column
if(isNowFocused){last_text=textField.getText(); System.out.println("NOW focus "+last_text);}
if (! isNowFocused && ! isValid(textField.getText())) {
textField.setText(last_text);
textField.selectAll();
System.out.println("blur");
}
});
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
// Show the Text Field
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
// myindizes.add(getIndex());
// Retrieve the actual String Property that should be bound to the TextField
// If the TextField is currently bound to a different StringProperty
// Unbind the old property and rebind to the new one
ObservableValue<String> ov = getTableColumn().getCellObservableValue(getIndex());
SimpleStringProperty sp = (SimpleStringProperty) ov;
if (this.boundToCurrently == null) {
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(sp);
} else if (this.boundToCurrently != sp) {
this.textField.textProperty().unbindBidirectional(this.boundToCurrently);
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(this.boundToCurrently);
}
double height = real_lines_height(textField.getText(), this.getWidth(), 30, 22);
textField.setPrefHeight(height);
textField.setMaxHeight(height);
textField.setMaxHeight(Double.MAX_VALUE);
// if height bigger than the biggest height in the row
//-> change all heights of the row(textfields ()typeof textarea) to this height
// else leave the height as it is
//System.out.println("item=" + item + " ObservableValue<String>=" + ov.getValue());
//this.textField.setText(item); // No longer need this!!!
} else {
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}//update
private boolean isValid(String s){
if(s.length()<7){return true;}
return false;
}
}
}
public class myTextRow {
private final SimpleIntegerProperty ID;
private final SimpleStringProperty text;
private final SimpleStringProperty text2;
public myTextRow(int ID, String text,String text2) {
this.ID = new SimpleIntegerProperty(ID);
this.text = new SimpleStringProperty(text);
this.text2 = new SimpleStringProperty(text2);
}
//setter
public void setID(int id) {
this.ID.set(id);
}
public void setText(String text) {
this.text.set(text);
}
public void setText2(String text) {
this.text2.set(text);
}
//getter
public int getID() {
return ID.get();
}
public String getText() {
return text.get();
}
public String getText2() {
return text2.get();
}
//properties
public StringProperty textProperty() {
return text;
}
public StringProperty text2Property() {
return text2;
}
public IntegerProperty IDProperty() {
return ID;
}
}
private static double real_lines_height(String s, double width, double heightCorrector, double widthCorrector) {
HBox h = new HBox();
Label l = new Label("Text");
h.getChildren().add(l);
Scene sc = new Scene(h);
l.applyCss();
double line_height = l.prefHeight(-1);
int new_lines = s.replaceAll("[^\r\n|\r|\n]", "").length();
// System.out.println("new lines= "+new_lines);
String[] lines = s.split("\r\n|\r|\n");
// System.out.println("line count func= "+ lines.length);
int count = 0;
//double rest=0;
for (int i = 0; i < lines.length; i++) {
double text_width = get_text_width(lines[i]);
double plus_lines = Math.ceil(text_width / (width - widthCorrector));
if (plus_lines > 1) {
count += plus_lines;
//rest+= (text_width / (width-widthCorrector)) - plus_lines;
} else {
count += 1;
}
}
//count+=(int) Math.ceil(rest);
count += new_lines - lines.length;
return count * line_height + heightCorrector;
}
private static double get_text_width(String s) {
HBox h = new HBox();
Label l = new Label(s);
l.setWrapText(false);
h.getChildren().add(l);
Scene sc = new Scene(h);
l.applyCss();
return l.prefWidth(-1);
}
}
There are probably (way) better ways to organize this, but probably the cleanest fix is just to define a boolean validate parameter to the constructor of your cell implementation. (You really don't want the logic to be "if the title of the column is equal to some specific text, then validate". You would be utterly screwed when your boss came in to the office and asked you to internationalize the application, or even just change the title of the column, for example.)
Using an entire inner class just to implement the callback seems completely redundant, but keeping that you would have to pass the parameter through it:
public static class TextFieldCellFactory
implements Callback<TableColumn<myTextRow, String>, TableCell<myTextRow, String>> {
private final boolean validate ;
public TextFieldCellFactory(boolean validate) {
this.validate = validate ;
}
#Override
public TableCell<myTextRow, String> call(TableColumn<myTextRow, String> param) {
TextFieldCell textFieldCell = new TextFieldCell(validate);
return textFieldCell;
}
public static class TextFieldCell extends TableCell<myTextRow, String> {
private TextArea textField;
private StringProperty boundToCurrently = null;
private String last_text;
public TextFieldCell(boolean validate) {
textField = new TextArea();
textField.setWrapText(true);
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
last_text="";
this.setGraphic(textField);
if (validate) {
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
//only if textfield is in the text2 column
if(isNowFocused){last_text=textField.getText(); System.out.println("NOW focus "+last_text);}
if (! isNowFocused && ! isValid(textField.getText())) {
textField.setText(last_text);
textField.selectAll();
System.out.println("blur");
}
});
}
}
// ...
}
Then of course you just do
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
clmtext.setCellFactory(new TextFieldCellFactory(false));
TableColumn<myTextRow, String> clmtext2 = new TableColumn<>("Text2");
clmtext2.setMinWidth(160);
clmtext2.setCellValueFactory(new PropertyValueFactory<>("text2"));
clmtext2.setCellFactory(new TextFieldCellFactory(true));
(To properly answer your question, you can get the text of the column from within the cell to which it is attached with getTableColumn().getText(), but as I pointed out, actually basing the logic on the value displayed in a column header will make your code completely unmaintainable.)
And I guess for completeness, I should also mention that your TextFieldCellFactory class looks like it is not really serving any purpose. I would remove it entirely and just have the TextFieldCell class, and do
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
clmtext.setCellFactory(c -> new TextFieldCell(false));
TableColumn<myTextRow, String> clmtext2 = new TableColumn<>("Text2");
clmtext2.setMinWidth(160);
clmtext2.setCellValueFactory(new PropertyValueFactory<>("text2"));
clmtext2.setCellFactory(c -> new TextFieldCell(true));
I used this code snippet to get my edit commit for my table done.
UITableView - Better Editing through Binding?
My problem is that i get an
Caused by: java.lang.ClassCastException: javafx.beans.property.ReadOnlyObjectWrapper cannot be cast to javafx.beans.property.SimpleStringProperty
in this line:
SimpleStringProperty sp = (SimpleStringProperty)ov;
i have no clue what i can do about that.
I'm using just SimpleStringProperty values for my data class.
Here is the full code:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextArea;
import javafx.util.Callback;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Supermain extends Application {
#Override
public void start(Stage primaryStage) {
final TableView<myTextRow> table = new TableView<>();
table.setEditable(true);
table.setStyle("-fx-text-wrap: true;");
//Table columns
TableColumn<myTextRow, String> clmID = new TableColumn<>("ID");
clmID.setMinWidth(160);
clmID.setCellValueFactory(new PropertyValueFactory<>("ID"));
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
clmtext.setCellFactory(new TextFieldCellFactory());
//Add data
final ObservableList<myTextRow> data = FXCollections.observableArrayList(
new myTextRow(5, "Lorem"),
new myTextRow(2, "Ipsum")
);
table.setItems(data);
table.getColumns().addAll(clmID, clmtext);
HBox hBox = new HBox();
hBox.setSpacing(5.0);
hBox.setPadding(new Insets(5, 5, 5, 5));
Button btn = new Button();
btn.setText("Get Data");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (myTextRow data1 : data) {
System.out.println("data:"+data1.getText());
}
}
});
hBox.getChildren().add(btn);
BorderPane pane = new BorderPane();
pane.setTop(hBox);
pane.setCenter(table);
primaryStage.setScene(new Scene(pane, 640, 480));
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public static class TextFieldCellFactory
implements Callback<TableColumn<myTextRow, String>, TableCell<myTextRow, String>> {
#Override
public TableCell<myTextRow, String> call(TableColumn<myTextRow, String> param) {
TextFieldCell textFieldCell = new TextFieldCell();
return textFieldCell;
}
public static class TextFieldCell extends TableCell<myTextRow, String> {
private TextArea textField;
private StringProperty boundToCurrently = null;
public TextFieldCell() {
String strCss;
// Padding in Text field cell is not wanted - we want the Textfield itself to "be"
// The cell. Though, this is aesthetic only. to each his own. comment out
// to revert back.
strCss = "-fx-padding: 0;";
this.setStyle(strCss);
textField = new TextArea();
textField.setWrapText(true);
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
//textField.setPrefHeight(real_lines_height(textField.getText(),this.getWidth(),30,23));
//
// Default style pulled from caspian.css. Used to play around with the inset background colors
// ---trying to produce a text box without borders
strCss = ""
+ //"-fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background;" +
"-fx-background-color: -fx-control-inner-background;"
+ //"-fx-background-insets: 0, 1, 2;" +
"-fx-background-insets: 0;"
+ //"-fx-background-radius: 3, 2, 2;" +
"-fx-background-radius: 0;"
+ // "-fx-padding: 3 5 3 5;" + /*Play with this value to center the text depending on cell height??*/
"-fx-padding: 0 0 0 0;"
+ /*Play with this value to center the text depending on cell height??*/ //"-fx-padding: 0 0 0 0;" +
"-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);"
+ "-fx-cursor: text;"
+ "";
// Focused and hover states should be set in the CSS. This is just a test
// to see what happens when we set the style in code
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
TextArea tf = (TextArea) getGraphic();
// System.out.println(".changed() index : "+ get_ID_from_table_index(getIndex()));
String strStyleGotFocus = "-fx-background-color: blue, -fx-text-box-border, -fx-control-inner-background;"
+ "-fx-background-insets: -0.4, 1, 2;"
+ "-fx-background-radius: 3.4, 2, 2;";
String strStyleLostFocus
= //"-fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background;" +
"-fx-background-color: -fx-control-inner-background;"
+ //"-fx-background-insets: 0, 1, 2;" +
"-fx-background-insets: 0;"
+ //"-fx-background-radius: 3, 2, 2;" +
"-fx-background-radius: 0;"
+ //"-fx-padding: 3 5 3 5;" + /**/
"-fx-padding: 0 0 0 0;"
+ /**/ //"-fx-padding: 0 0 0 0;" +
"-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);"
+ "-fx-cursor: text;"
+ "";
if (newValue.booleanValue()) {
tf.setStyle(strStyleGotFocus);
} else {
tf.setStyle(strStyleLostFocus);
}
if(!newValue)
{
System.out.println("EDITABLE???? "+isEditing());
System.out.println("TEXT:::: "+textField.getText());
// commitEdit(textField.getText());
}
}
});
textField.hoverProperty().addListener(new ChangeListener<Boolean>() {
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
TextArea tf = (TextArea) getGraphic();
String strStyleGotHover = "-fx-background-color: derive(blue,90%), -fx-text-box-border, derive(-fx-control-inner-background, 10%);"
+ "-fx-background-insets: 1, 2.8, 3.8;"
+ "-fx-background-radius: 3.4, 2, 2;";
String strStyleLostHover
= //"-fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background;" +
"-fx-background-color: -fx-control-inner-background;"
+ //"-fx-background-insets: 0, 1, 2;" +
"-fx-background-insets: 0;"
+ //"-fx-background-radius: 3, 2, 2;" +
"-fx-background-radius: 0;"
+ //"-fx-padding: 3 5 3 5;" + /**/
"-fx-padding: 0 0 0 0;"
+ "-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);"
+ "-fx-cursor: text;"
+ "";
String strStyleHasFocus = "-fx-background-color: blue, -fx-text-box-border, -fx-control-inner-background;"
+ "-fx-background-insets: -0.4, 1, 2;"
+ "-fx-background-radius: 3.4, 2, 2;";
if (newValue.booleanValue()) {
tf.setStyle(strStyleGotHover);
} else if (!tf.focusedProperty().get()) {
tf.setStyle(strStyleLostHover);
} else {
tf.setStyle(strStyleHasFocus);
}
}
});
textField.textProperty().addListener(e -> {
double height = 25;
textField.setPrefHeight(height);
textField.setMaxHeight(height);
//System.out.println("textfield Parent: "+textField.getParent().toString());
});
textField.setStyle(strCss);
this.setGraphic(textField);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(!empty) {
// Show the Text Field
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
// Retrieve the actual String Property that should be bound to the TextField
// If the TextField is currently bound to a different StringProperty
// Unbind the old property and rebind to the new one
ObservableValue<String> ov = getTableColumn().getCellObservableValue(getIndex());
SimpleStringProperty sp = (SimpleStringProperty)ov;
if(this.boundToCurrently==null) {
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(sp);
}
else {
if(this.boundToCurrently != sp) {
this.textField.textProperty().unbindBidirectional(this.boundToCurrently);
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(this.boundToCurrently);
}
}
System.out.println("item=" + item + " ObservableValue<String>=" + ov.getValue());
//this.textField.setText(item); // No longer need this!!!
}
else {
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
}
public class myTextRow {
private final SimpleIntegerProperty ID;
private final SimpleStringProperty text;
public myTextRow(int ID, String text) {
this.ID = new SimpleIntegerProperty(ID);
this.text = new SimpleStringProperty(text);
}
public void setID(int id) {
this.ID.set(id);
}
public void setText(String text) {
this.text.set(text);
}
public int getID() {
return ID.get();
}
public String getText() {
return text.get();
}
}
}
Your model class is missing the "property accessors". Consequently, the properties themselves cannot be used by the PropertyValueFactory. As stated in the PropertyValueFactory documentation:
An example of how to use this class is:
TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name");
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("firstName"));
In this example,
the "firstName" string is used as a reference to an assumed
firstNameProperty() method in the Person class type (which is the
class type of the TableView items list). Additionally, this method
must return a Property instance. If a method meeting these
requirements is found, then the TableCell is populated with this
ObservableValue. In addition, the TableView will automatically add an
observer to the returned value, such that any changes fired will be
observed by the TableView, resulting in the cell immediately updating.
If no method matching this pattern exists, there is fall-through
support for attempting to call get() or is() (that
is, getFirstName() or isFirstName() in the example above). If a method
matching this pattern exists, the value returned from this method is
wrapped in a ReadOnlyObjectWrapper and returned to the TableCell.
The last paragraph describes exactly your situation, because you have no textProperty() or iDProperty() methods defined in your model class. Hence the PropertyValueFactory creates a ReadOnlyObjectWrapper for you and returns it, instead of returning the actual property instance.
Note also you have the wrong type for the id column. The property is an IntegerProperty, which is a Property<Number>, not a Property<String>. Consequently that table column needs to be a TableColumn<myTextRow, Number>. This will make it quite a bit trickier to use the table cell implementation, as you need to convert between the String in the text field and the Integer that is the value for id.
In general though, write the class following the JavaFX Property pattern as follows:
public class myTextRow {
private final IntegerProperty id;
private final StringProperty text;
public myTextRow(int ID, String text) {
this.id = new SimpleIntegerProperty(ID);
this.text = new SimpleStringProperty(text);
}
public void setId(int id) {
this.id.set(id);
}
public void setText(String text) {
this.text.set(text);
}
public int getId() {
return id.get();
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public IntegerProperty idProperty() {
return id ;
}
}
and then you can define your cell value factories as
TableColumn<myTextRow, Number> clmID = new TableColumn<>("ID");
clmID.setMinWidth(160);
clmID.setCellValueFactory(new PropertyValueFactory<>("id"));
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
or, using lambda expressions (which makes the code typesafe and allows the compiler to check for the existence of the correct methods):
TableColumn<myTextRow, Number> clmID = new TableColumn<>("ID");
clmID.setMinWidth(160);
clmID.setCellValueFactory(cellData -> cellData.getValue().idProperty());
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(cellData -> cellData.getValue().textProperty());
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);
}
});