Update: inserted project (reproductible example) and last details
I have an application that is displaying a TableView filled with simple objects (observable list)
I want to display selected items (rows) in a TableView highlighting them.
Ex: If the user press 'Insert' i update (in the observable list) the object which is selected. A boolean in the object will do. The objects are 'marked'; the user can do something else.
I cannot use myTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); as the user will loose the selection as soon as a key is pressed or a mouse clik happens.
With that in mind, this means i'm managing keyboard like so:
public boolean implementListenerPackage(Scene s) {
//some init then...
s.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent ke) {
switch (ke.getCode()) {
case INSERT:
setObservableListObjectSelect();
break;
}
}
});
}
The object in the observable list is rather simple:
public class myObject {
private boolean selected;
private String otherStuff = "";
// Then constructor , getters and setters
And i have a MouseEvent management to handle other actions as well. When i'm creating my TableView i add this:
myTableView.setRowFactory(rftv-> {
TableRow<type> rowObj = new TableRow<>();
rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
SomeClass.doSomethingForDoubleClik()
} else { // Simple clic
SomeClass.doSomethingForSimpleClik()
}
}
});
return rowObj;
});
My goal is to change the CSS of a row when the myObject boolean changes. And by doing so make the user selection highlighted even though the user click on another row.
I tried :
A lot of reading on this website, but only to find simple examples. Not the one i have with several obstacles at the same time.
to implement more things in the rowFactory. But i couldn't do it. If it compiles, then it crashes with a nullpointerexception. I never been good at those tricky syntax things.
to do it directly from the keyboard management but only to find it's rather complicated. I have to get the selected object, update it, then find the selected cells and change the CSS one by one (column logic).
to implement a "binding" (An example here) between the object and the row but only to find myself wondering how to implement it as it was an answer for a different problem 'flavor'.
It's probably in front of my eyes but i can't see it.
update:
Bear in mind that
the keyboard management is centralized.
there is already a factory set on the tableView.
They are several TableView in the original application so i whish the CSS style change automatically and not in some kind of 'hardcoded' fashion.
The minimal code:
package application;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.stage.Stage;
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.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
public class Main extends Application {
Label lbl01 = new Label("Information");
#Override
public void start(Stage primaryStage) {
try {
TableView tv1 = new TableView();
TableColumn<MyObject, String> column1 = new TableColumn<>("Col 01");
column1.setCellValueFactory(new PropertyValueFactory<>("keyboardSelected"));
TableColumn<MyObject, String> column2 = new TableColumn<>("Col 02");
column2.setCellValueFactory(new PropertyValueFactory<>("dataA"));
TableColumn<MyObject, String> column3 = new TableColumn<>("Col 03");
column3.setCellValueFactory(new PropertyValueFactory<>("dataB"));
TableColumn<MyObject, String> column4 = new TableColumn<>("Col 04");
column4.setCellValueFactory(new PropertyValueFactory<>("dataC"));
tv1.getColumns().add(column1);
tv1.getColumns().add(column2);
tv1.getColumns().add(column3);
tv1.getColumns().add(column4);
ObservableList<MyObject> olm1 = FXCollections.observableArrayList();
olm1.addAll(new MyObject(false, "Object01 A", "Object01 B", "Object01 C"),
new MyObject(false, "Object02 A", "Object02 B", "Object02 C"),
new MyObject(false, "Object03 A", "Object03 B", "Object03 C"),
new MyObject(false, "Object04 A", "Object04 B", "Object04 C"),
new MyObject(false, "Object05 A", "Object05 B", "Object05 C")
);
tv1.setItems(olm1);
tv1.setRowFactory(dc -> {
TableRow<MyObject> rowObj = new TableRow<>();
rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
lbl01.setText("Double click on line " + tv1.getSelectionModel().getSelectedIndex());
} else {
lbl01.setText("Single click on line " + +tv1.getSelectionModel().getSelectedIndex());
}
}
});
return rowObj;
});
VBox root = new VBox(tv1, lbl01);
Scene scene = new Scene(root, 512, 640);
primaryStage.setScene(scene);
KeyboardManagement km = new KeyboardManagement();
km.implementListener(scene, lbl01, tv1);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
package application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class KeyboardManagement {
public KeyboardManagement() {
}
public boolean implementListener(Scene s, Label l, TableView tv1) {
boolean retRep = false;
try {
s.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent ke) {
if (ke.getCode() == KeyCode.SPACE) {
MyObject m1 = (MyObject) tv1.getSelectionModel().getSelectedItem();
tv1.refresh();
m1.setKeyboardSelected(!m1.isKeyboardSelected());
l.setText("Space was pressed / TableView Line :" + tv1.getSelectionModel().getSelectedIndex()
+ ". " + m1.toString());
}
}
});
} catch (Exception e) {
}
return retRep;
}
}
package application;
public class MyObject {
private boolean keyboardSelected;
private String dataA;
private String dataB;
private String dataC;
public MyObject(boolean keyboardSelected, String dataA, String dataB, String dataC) {
super();
this.keyboardSelected = keyboardSelected;
this.dataA = dataA;
this.dataB = dataB;
this.dataC = dataC;
}
public boolean isKeyboardSelected() {
return keyboardSelected;
}
public void setKeyboardSelected(boolean keyboardSelected) {
this.keyboardSelected = keyboardSelected;
}
public String getDataA() {
return dataA;
}
public void setDataA(String dataA) {
this.dataA = dataA;
}
public String getDataB() {
return dataB;
}
public void setDataB(String dataB) {
this.dataB = dataB;
}
public String getDataC() {
return dataC;
}
public void setDataC(String dataC) {
this.dataC = dataC;
}
#Override
public String toString() {
return "MyObject [keyboardSelected=" + keyboardSelected + ", dataA=" + dataA + ", dataB=" + dataB + ", dataC="
+ dataC + "]";
}
}
Found it!
Simple and complex at the same time.
The trick is to insert at the right place a listener on the object property.
Inside the rowfactory definition.
Before the #override
Pass the row object as an argument and 'voila'.
Something like :
rowObj.itemProperty().addListener((observable, oldValue, newValue) -> updateTableRowCss(rowObj, newValue));
Notes:
You can add several listeners on different object properties.
I strongly suggest you create a method the listener will call. Sometimes the IDE syntax analyzers are making bloodshed in the screen. It doesn't help finding errors in this type of code.
From the tests i made, it seems that the modifications on the style you do will preval (like more important) on the already loaded style. Which is fine.
This way you keep a centralized keyboard management (like in this example) and the mouse event management in the factory.
The cursor still moves freely and you keep an object selection you can reuse later.
The modified example code from above:
package application;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.stage.Stage;
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.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
public class Main extends Application {
Label lbl01 = new Label("Information");
#Override
public void start(Stage primaryStage) {
try {
TableView tv1 = new TableView();
TableColumn<MyObject, String> column1 = new TableColumn<>("Col 01");
column1.setCellValueFactory(new PropertyValueFactory<>("keyboardSelected"));
TableColumn<MyObject, String> column2 = new TableColumn<>("Col 02");
column2.setCellValueFactory(new PropertyValueFactory<>("dataA"));
TableColumn<MyObject, String> column3 = new TableColumn<>("Col 03");
column3.setCellValueFactory(new PropertyValueFactory<>("dataB"));
TableColumn<MyObject, String> column4 = new TableColumn<>("Col 04");
column4.setCellValueFactory(new PropertyValueFactory<>("dataC"));
tv1.getColumns().add(column1);
tv1.getColumns().add(column2);
tv1.getColumns().add(column3);
tv1.getColumns().add(column4);
ObservableList<MyObject> olm1 = FXCollections.observableArrayList();
olm1.addAll(new MyObject(false, "Object01 A", "Object01 B", "Object01 C"),
new MyObject(false, "Object02 A", "Object02 B", "Object02 C"),
new MyObject(false, "Object03 A", "Object03 B", "Object03 C"),
new MyObject(false, "Object04 A", "Object04 B", "Object04 C"),
new MyObject(false, "Object05 A", "Object05 B", "Object05 C")
);
tv1.setItems(olm1);
tv1.setRowFactory(dc -> {
TableRow<MyObject> rowObj = new TableRow<>();
rowObj.itemProperty().addListener((observable, oldValue, newValue) -> updateTableRowCss(rowObj, newValue));
rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
lbl01.setText("Double click on line " + tv1.getSelectionModel().getSelectedIndex());
} else {
lbl01.setText("Single click on line " + +tv1.getSelectionModel().getSelectedIndex());
}
}
});
return rowObj;
});
VBox root = new VBox(tv1, lbl01);
Scene scene = new Scene(root, 512, 640);
primaryStage.setScene(scene);
KeyboardManagement km = new KeyboardManagement();
km.implementListener(scene, lbl01, tv1);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private void updateTableRowCss(TableRow<MyObject> rowObj, MyObject item) {
// On doit vérifier si null
if (item != null ) {
if (item.isKeyboardSelected()) {
rowObj.setStyle("-fx-background-color: #FF000080;");
} else {
rowObj.setStyle("");
}
}
}
}
Final result
Related
I am revising my question based on the comments. I have a JavaFX TableView for which the number of checkbox columns is only known at runtime. So, to create the columns, I do:
TableColumn attributeColumn = new TableColumn("Attribut");
attributeColumn.setCellValueFactory(new PropertyValueFactory<AttributeRow, String>("name"));
attributeTable.getColumns().add(attributeColumn);
for (String group : companyGroups)
{
TableColumn< AttributeRow, Boolean > groupColumn = new TableColumn<>( group );
groupColumn.setCellFactory(CheckBoxTableCell.forTableColumn(groupColumn));
groupColumn.setCellValueFactory( f -> f.getValue().activeProperty());
groupColumn.setEditable(true);
attributeTable.getColumns().add(groupColumn);
}
The question is, how would a table model look like for this TableView? If there were a fixed number of checkbox columns, say 2 columns, my model looks like this:
public class AttributeRow {
private SimpleStringProperty name;
private SimpleBooleanProperty active = new SimpleBooleanProperty(false);
public AttributeRow(String name, Boolean active) {
this.name= new SimpleStringProperty(name);
}
public SimpleStringProperty nameProperty() {
if (name == null) {
name = new SimpleStringProperty(this, "name");
}
return name;
}
public String getAttributeName() {
return name.get();
}
public void setAttributeName(String fName) {
name.set(fName);
}
public final SimpleBooleanProperty activeProperty() {
return this.active;
}
public final boolean isActive() {
return this.activeProperty().get();
}
public final void setActive(final boolean active) {
this.activeProperty().set(active);
}
}
This model works if I have one string column and one checkbox column. But what do I do, if I have muliple checkbox columns for which the number is only known at runtime?
You haven't really described the structure of the data, but it looks like there is some kind of collection of Strings (companyGroups) and each row is the table is represented by a String (name) and one boolean for each element of companyGroups. So one way to do this would just be to define a Map<String, BooleanProperty> in the model class AttributeRow, where the key in the map is intended to be an element of companyGroups:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class AttributeRow {
private final StringProperty name = new SimpleStringProperty();
private final Map<String, BooleanProperty> activeByGroup = new HashMap<>();
public AttributeRow(List<String> companyGroups) {
for (String group : companyGroups) {
activeByGroup.put(group, new SimpleBooleanProperty()) ;
}
}
public final BooleanProperty activeProperty(String group) {
// might need to deal with the case where
// there is no entry in the map for group
// (else calls to isActive(...) and setActive(...) with
// a non-existent group will give a null pointer exception):
return activeByGroup.get(group) ;
}
public final boolean isActive(String group) {
return activeProperty(group).get();
}
public final void setActive(String group, boolean active) {
activeProperty(group).set(active);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
}
There is nothing special about the cell value factory for the columns - it still just has to map each row to the appropriate observable property for the column:
for (String group : groups) {
TableColumn<AttributeRow, Boolean> groupColumn = new TableColumn<>(group);
groupColumn.setCellFactory(CheckBoxTableCell.forTableColumn(groupColumn));
groupColumn.setCellValueFactory(cellData -> cellData.getValue().activeProperty(group));
attributeTable.getColumns().add(groupColumn);
}
and of course to update values you just update the model:
Button selectAll = new Button("Select all");
selectAll.setOnAction(e -> {
for (AttributeRow row : attributeTable.getItems()) {
for (String group : groups) {
row.setActive(group, true);
}
}
});
Here is a SSCCE:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class TableWithMappedBooleans extends Application {
private static final List<String> groups = Arrays.asList("Group 1", "Group 2", "Group 3", "Group 4");
#Override
public void start(Stage primaryStage) {
TableView<AttributeRow> attributeTable = new TableView<>();
attributeTable.setEditable(true);
TableColumn<AttributeRow, String> attributeColumn = new TableColumn<>("Attribute");
attributeColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
attributeTable.getColumns().add(attributeColumn);
for (String group : groups) {
TableColumn<AttributeRow, Boolean> groupColumn = new TableColumn<>(group);
groupColumn.setCellFactory(CheckBoxTableCell.forTableColumn(groupColumn));
groupColumn.setCellValueFactory(cellData -> cellData.getValue().activeProperty(group));
attributeTable.getColumns().add(groupColumn);
}
// generate data:
for (int i = 1 ; i <= 10; i++) {
AttributeRow row = new AttributeRow(groups);
row.setName("Attribute "+i);
attributeTable.getItems().add(row);
}
// button to select everything:
Button selectAll = new Button("Select all");
selectAll.setOnAction(e -> {
for (AttributeRow row : attributeTable.getItems()) {
for (String group : groups) {
row.setActive(group, true);
}
}
});
// for debugging, to check data are updated from check boxes:
Button dumpDataButton = new Button("Dump data");
dumpDataButton.setOnAction(e -> {
for (AttributeRow row : attributeTable.getItems()) {
String groupList = groups.stream()
.filter(group -> row.isActive(group))
.collect(Collectors.joining(", "));
System.out.println(row.getName() + " : " + groupList);
}
System.out.println();
});
HBox buttons = new HBox(5, selectAll, dumpDataButton);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(5));
BorderPane root = new BorderPane(attributeTable, null, null, buttons, null);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I got another JavaFX problem. To keep the story of the problem very short:
I have two TableViews with items that I want to drag and drop in the respective TableView. The problem that I'm encountering at the momemt is that I can drop the item in the same TableView where I got it from. That should not be the case and I want to prevent this.
Is there a way to restrict the target table and, if so, what do I have to do?
Thanks a lot for your help!
implemented setOnDragOver method:
selectedParticipantsTable.setOnDragOver(new EventHandler<DragEvent>() {
#Override
public void handle(DragEvent event) {
// data is dragged over the target
Dragboard db = event.getDragboard();
if (db.hasContent(participantsDataFormat)){
if(selectedParticipantsTableData.contains(db.getContent(participantsDataFormat)) != true){
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
else
{
event.acceptTransferModes(TransferMode.NONE);
}
}
event.consume();
}
});
Ok, I think I got a bit further. I created an own DataFormat to put the items in the ClipBoard of the DragBoard. The problem now is that the SimpleStringProperties in the MetaData class are not serializable and I don't know how to this properly as I always get an EOFExceptio when I drag the item over the other TableView.
Any suggestions?
The restriction that you can only put serializable objects onto a dragboard is a real pain. Even if you make your model class serializable (which is difficult, because JavaFX properties are not serializable, but not impossible), it's probably not going to do what you want, as you will get a copy of the object on dragging instead of a reference to the original object.
The only workaround I have is essentially to store the dragged object in an instance variable (if the code for dropping is in a different class to the code for dragging, then you need something more convoluted).
Here is an SSCCE using the usual contact table from the Oracle tutorial:
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.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class TwoTablesDragging extends Application {
private static final String DRAGGING_PERSON_KEY = "dragging-person";
private Person currentDraggedPerson ;
#Override
public void start(Stage primaryStage) {
TableView<Person> contacts = createPersonTable();
TableView<Person> selectedContacts = createPersonTable();
contacts.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")
);
HBox root = new HBox(10, contacts, selectedContacts);
root.setPadding(new Insets(10));
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
private void setUpDragAndDrop(TableView<Person> table) {
// note: It's generally better to set drag detected on the table rows, using
// a rowFactory, so you don't rely on selection. This is just a "quick and dirty"
// approach for a demo
table.setOnDragDetected(e -> {
Dragboard db = table.startDragAndDrop(TransferMode.COPY_OR_MOVE);
ClipboardContent content = new ClipboardContent();
content.putString(DRAGGING_PERSON_KEY);
db.setContent(content);
currentDraggedPerson = table.getSelectionModel().getSelectedItem();
});
table.setOnDragOver(e -> {
Dragboard db = e.getDragboard();
if (DRAGGING_PERSON_KEY.equals(db.getString()) &&
! table.getItems().contains(currentDraggedPerson)) {
e.acceptTransferModes(TransferMode.MOVE);
}
});
table.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
if (DRAGGING_PERSON_KEY.equals(db.getString())) {
table.getItems().add(currentDraggedPerson);
e.setDropCompleted(true);
} else {
e.setDropCompleted(false);
}
});
table.setOnDragDone(e -> {
if (e.getTransferMode() == TransferMode.MOVE) {
table.getItems().remove(currentDraggedPerson);
currentDraggedPerson = null ;
}
});
}
private TableView<Person> createPersonTable() {
TableView<Person> table = new TableView<>();
table.getColumns().add(column("First Name", Person::firstNameProperty, 100));
table.getColumns().add(column("Last Name", Person::lastNameProperty, 100));
table.getColumns().add(column("Email", Person::emailProperty, 175));
setUpDragAndDrop(table);
return table ;
}
private <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> prop, double width) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> prop.apply(cellData.getValue()));
col.setPrefWidth(width);
return col ;
}
public static class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private 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);
}
}
I have an editable JavaFX ComboBox.
A user must only be able to
type alphabets ('a' to 'z'), space and round braces ('(', ')') to enter the string
press tab to exit
press enter to exit
How to filter out every other key, modifiers etc?
I have read about and used event handlers like Key_Pressed, Key_Released but I am unable to figure out a straight-forward way to achieve the above.
I am using Mac OS Yosemite, Java 8, latest version of JavaFX and
public static final EventType<KeyEvent> KEY_TYPED just does not work at all.
Below code is my attempt. The variable typedText stores the desired values.
comboBox.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
private final String[] allowedItems = new String[]{"a","b","c","d","e","f",
"g","h","i","j","k","l","m","n","o","p","q","r",
"s","t","u","v","w","x","y","z"," ","(",")"};
private final List data = Arrays.asList(allowedItems);
private String tempInput;
public boolean containsCaseInsensitive(String s, List<String> l){
for (String string : l) {
if (string.equalsIgnoreCase(s)){
return true;
}
}
return false;
}
public void handle(KeyEvent event) {
boolean b;
b = event.isShiftDown();
if (b) {
if (event.getText().equals("(")) {
tempInput = "(";
} else if (event.getText().equals(")")){
tempInput = ")";
}
} else {
tempInput = event.getCode().toString().toLowerCase();
}
System.out.println("tempInput:"+tempInput);
if (containsCaseInsensitive(tempInput, data)) {
typedText = tempInput;
System.out.println("typedText:"+typedText);
}
}
});
}
You can get the Editor, which is a TextField in your case, and add a TextFormatter to it which restricts the input.
Tab works out of the box, but the "enter" keypress is a different matter, I simply request the focus in this example. Usually you'd navigate to the next item in the focus traversal list, but there's no future-proof api for that yet in JavaFX.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ComboBoxSample extends Application {
#Override
public void start(Stage stage) {
ComboBox<String> comboBox = new ComboBox<>();
comboBox.setEditable(true);
comboBox.getItems().addAll("A", "B", "C", "D", "E");
comboBox.setValue("A");
// restrict input
TextField textField = comboBox.getEditor();
TextFormatter<String> formatter = new TextFormatter<String>(change -> {
change.setText(change.getText().replaceAll("[^a-z ()]", ""));
return change;
});
textField.setTextFormatter(formatter);
// dummy textfield to jump to on ENTER press
TextField dummyTextField = new TextField();
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if( e.getCode() == KeyCode.ENTER) {
dummyTextField.requestFocus();
e.consume();
}
});
HBox root = new HBox();
root.getChildren().addAll(comboBox, dummyTextField);
Scene scene = new Scene(root, 450, 250);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have an event listener on a TableView that listens for keyboard event.
// Add event listener to table
table.setOnKeyTyped(event -> {
TablePosition<SimpleStringProperty, String> focusedCell = table.getFocusModel().getFocusedCell();
if (focusedCell != null)
{
table.getItems().get(focusedCell.getRow()).set(event.getCharacter());
table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
});
I am having problems with updating the cell with the new data when a user clicks enter or changes focus to another cell. When you click enter or change focus, the cell becomes empty. I'm not sure why. How can I save the data and update the cell with the new data.
// Here is the full code.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewEdit extends Application
{
#Override
public void start(Stage primaryStage)
{
TableView<SimpleStringProperty> table = new TableView<SimpleStringProperty>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(this.createColumn());
ObservableList<SimpleStringProperty> rowData = FXCollections.observableArrayList();
//table.getItems().addAll(rowData);
for (int j = 0; j < 10; j++)
{
rowData.add(new SimpleStringProperty(String.format("Cell [%d", j)));
}
table.setItems(rowData);
table.setOnKeyTyped(event -> {
TablePosition<SimpleStringProperty, String> focusedCell = table.getFocusModel().getFocusedCell();
if (focusedCell != null)
{
table.getItems().get(focusedCell.getRow()).set(event.getCharacter());
table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
});
Scene scene = new Scene(new BorderPane(table), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<SimpleStringProperty, String> createColumn()
{
TableColumn<SimpleStringProperty, String> col = new TableColumn<>("Column ");
col.setCellValueFactory(cellData -> cellData.getValue());
col.setCellFactory(column -> new EditCell());
return col;
}
private static class EditCell extends TableCell<SimpleStringProperty, String>
{
private final TextField textField = new TextField();
EditCell()
{
this.textProperty().bind(this.itemProperty());
this.setGraphic(this.textField);
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
this.textField.setOnAction(evt -> this.commitEdit(this.textField.getText()));
this.textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused)
{
this.commitEdit(this.textField.getText());
}
});
}
#Override
public void startEdit()
{
super.startEdit();
this.textField.setText(this.getItem());
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
this.textField.requestFocus();
}
#Override
public void cancelEdit()
{
super.cancelEdit();
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String text)
{
super.commitEdit(text);
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
public static void main(String[] args)
{
launch(args);
}
}
These get really tricky; I think anything "behavior-related" (i.e. standard controls reacting to user input) is hard to change and generally not well supported in JavaFX. Hopefully this is an area of the API that will be improved...
There seem to be a couple of different issues. I think that what is happening with the Enter key, is that although this generates an ActionEvent on the text field, which commits the edit, etc, the keyTyped event still propagates back to the table, causing it to re-enter editing mode. A fix for this seems to be to use a keyPressed handler on the table instead (though to be honest this doesn't feel very robust).
The code relies on the default onEditCommit handler on the table column to actually change the property value. The onEditCommit handler is invoked by the default table cell's commitEdit method. The problem with calling commitEdit(...) on losing focus is that the default commitEdit method first checks if the cell is in an editing state, and does nothing if it's not. It appears that when the cell loses focus, it is taken out of the editing state before the focusProperty listener is invoked, so the onEditCommit handler is never called. (As an aside, this also prevents example 13-11 "Alternative solution of cell editing" (sic) from working correctly in the JDK 8 u25 (the current version).)
The only fix I can see for this second issue is to directly update the property from the commitEdit(...) method. This requires the cell have a reference to the property, which breaks the nice separation between the cell and the cell value.
I rewrote the example using the usual Person example and incorporated these two fixes. This example works quite well, though as I said some parts feel as though they are not very robust:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewEditOnType extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
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.setOnKeyPressed(event -> {
TablePosition<Person, ?> pos = table.getFocusModel().getFocusedCell() ;
if (pos != null) {
table.edit(pos.getRow(), pos.getTableColumn());
}
});
Scene scene = new Scene(new BorderPane(table), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<Person, String> createColumn(String title, Function<Person, StringProperty> property) {
TableColumn<Person, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> new EditCell(property));
return col ;
}
private static class EditCell extends TableCell<Person, String> {
private final TextField textField = new TextField();
private final Function<Person, StringProperty> property ;
EditCell(Function<Person, StringProperty> property) {
this.property = property ;
textProperty().bind(itemProperty());
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt -> {
commitEdit(textField.getText());
});
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (! isNowFocused) {
commitEdit(textField.getText());
}
});
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(getItem());
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String text) {
super.commitEdit(text);
Person person = getTableView().getItems().get(getIndex()) ;
StringProperty cellProperty = property.apply(person);
cellProperty.set(text);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
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);
}
}
After doing a Oracle tutorial about the TableView, I was wondering if there's a way to programmatically apply different CSS style to the selected TableView row. For example, user selects a certain row, clicks the "Highlight" button and the selected row gets brown background, white text fill, etc. I've read the JavaFX tableview colors, Updating TableView row appearance and Background with 2 colors in JavaFX?, but to no avail =/
Here's the source:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
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")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(600);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final Button btnHighlight = new Button("Highlight selected row");
btnHighlight.setMaxWidth(Double.MAX_VALUE);
btnHighlight.setPrefHeight(30);
btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent e){
// this is where the CSS should be applied
}
});
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table, btnHighlight);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
And the application.css from which the "Highlight selected row" button applies the highlightedRow class to the selected table row:
.highlightedRow {
-fx-background-color: brown;
-fx-background-insets: 0, 1, 2;
-fx-background: -fx-accent;
-fx-text-fill: -fx-selection-bar-text;
}
Edit:
After several hours of trying, the best thing I could come up is this using the code below:
firstNameCol.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
#Override
public TableCell<Person, String> call(TableColumn<Person, String> personStringTableColumn) {
return new TableCell<Person, String>() {
#Override
protected void updateItem(String name, boolean empty) {
super.updateItem(name, empty);
if (!empty) {
if (name.toLowerCase().startsWith("e") || name.toLowerCase().startsWith("i")) {
getStyleClass().add("highlightedRow");
}
setText(name);
} else {
setText("empty"); // for debugging purposes
}
}
};
}
});
The part I don't really understand is why I can't do that from inside the setOnAction method of the btnHighlight? I also tried refreshing the table afterwards (described here), but it didn't seem to work. Also, my "solution" only works for the firstNameCol column, so does one have to set new cell factory for each column in order to apply a certain style, or is there a smarter solution?
Edit: Updated version of this (old) post is at https://stackoverflow.com/a/73764770/2189127
If you don't want the reusability of the solution I posted here, this is really the same thing but using an anonymous inner class for the row factory instead of a standalone class. Perhaps the code is easier to follow as it's all in one place. It's kind of a hybrid between Jonathan's solution and mine, but will automatically update the highlights without forcing it with a sort.
I used a list of integers so it supports multiple selection, but if you don't need that you could obviously just use an IntegerProperty instead.
Here's the row factory:
final ObservableList<Integer> highlightRows = FXCollections.observableArrayList();
table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
#Override
public TableRow<Person> call(TableView<Person> tableView) {
final TableRow<Person> row = new TableRow<Person>() {
#Override
protected void updateItem(Person person, boolean empty){
super.updateItem(person, empty);
if (highlightRows.contains(getIndex())) {
if (! getStyleClass().contains("highlightedRow")) {
getStyleClass().add("highlightedRow");
}
} else {
getStyleClass().removeAll(Collections.singleton("highlightedRow"));
}
}
};
highlightRows.addListener(new ListChangeListener<Integer>() {
#Override
public void onChanged(Change<? extends Integer> change) {
if (highlightRows.contains(row.getIndex())) {
if (! row.getStyleClass().contains("highlightedRow")) {
row.getStyleClass().add("highlightedRow");
}
} else {
row.getStyleClass().removeAll(Collections.singleton("highlightedRow"));
}
}
});
return row;
}
});
And here are what some buttons might look like:
final Button btnHighlight = new Button("Highlight");
btnHighlight.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedIndices()));
btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
highlightRows.setAll(table.getSelectionModel().getSelectedIndices());
}
});
final Button btnClearHighlight = new Button("Clear Highlights");
btnClearHighlight.disableProperty().bind(Bindings.isEmpty(highlightRows));
btnClearHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
highlightRows.clear();
}
});
How about creating a row factory which exposes an observable list of the indexes of table rows which are to be highlighted? That way you can simply update the list with the indexes you need to highlight: for example by calling the getSelectedIndices() on the selection model and passing it to the list's setAll(...) method.
This could look something like:
import java.util.Collections;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;
public class StyleChangingRowFactory<T> implements
Callback<TableView<T>, TableRow<T>> {
private final String styleClass ;
private final ObservableList<Integer> styledRowIndices ;
private final Callback<TableView<T>, TableRow<T>> baseFactory ;
public StyleChangingRowFactory(String styleClass, Callback<TableView<T>, TableRow<T>> baseFactory) {
this.styleClass = styleClass ;
this.baseFactory = baseFactory ;
this.styledRowIndices = FXCollections.observableArrayList();
}
public StyleChangingRowFactory(String styleClass) {
this(styleClass, null);
}
#Override
public TableRow<T> call(TableView<T> tableView) {
final TableRow<T> row ;
if (baseFactory == null) {
row = new TableRow<>();
} else {
row = baseFactory.call(tableView);
}
row.indexProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> obs,
Number oldValue, Number newValue) {
updateStyleClass(row);
}
});
styledRowIndices.addListener(new ListChangeListener<Integer>() {
#Override
public void onChanged(Change<? extends Integer> change) {
updateStyleClass(row);
}
});
return row;
}
public ObservableList<Integer> getStyledRowIndices() {
return styledRowIndices ;
}
private void updateStyleClass(TableRow<T> row) {
final ObservableList<String> rowStyleClasses = row.getStyleClass();
if (styledRowIndices.contains(row.getIndex()) ) {
if (! rowStyleClasses.contains(styleClass)) {
rowStyleClasses.add(styleClass);
}
} else {
// remove all occurrences of styleClass:
rowStyleClasses.removeAll(Collections.singleton(styleClass));
}
}
}
Now you can do
final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<>("highlightedRow");
table.setRowFactory(rowFactory);
And in your button's action handler do
rowFactory.getStyledRowIndices().setAll(table.getSelectionModel().getSelectedIndices());
Because StyleChangingRowFactory wraps another row factory, you can still use it if you already have a custom row factory implementation you want to use. For example:
final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<Person>(
"highlightedRow",
new Callback<TableView<Person>, TableRow<Person>>() {
#Override
public TableRow<Person> call(TableView<Person> tableView) {
final TableRow<Person> row = new TableRow<Person>();
ContextMenu menu = new ContextMenu();
MenuItem removeMenuItem = new MenuItem("Remove");
removeMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
table.getItems().remove(row.getItem());
}
});
menu.getItems().add(removeMenuItem);
row.contextMenuProperty().bind(
Bindings.when(row.emptyProperty())
.then((ContextMenu) null)
.otherwise(menu));
return row;
}
});
table.setRowFactory(rowFactory);
Here is a complete code example.
Here's an ugly hack solution. Firstly, define an int field called highlightedRow. Then set a row factory on the TableView:
table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
#Override public TableRow<Person> call(TableView<Person> param) {
return new TableRow<Person>() {
#Override protected void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
if (getIndex() == highlightedRow) {
getStyleClass().add("highlightedRow");
} else {
getStyleClass().remove("highlightedRow");
}
}
};
}
});
Then add the following code in your button on action (and this is where the ugly hack comes into play):
btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent e){
// set the highlightedRow integer to the selection index
highlightedRow = table.getSelectionModel().getSelectedIndex();
// force a tableview refresh - HACK
List<Person> items = new ArrayList<>(table.getItems());
table.getItems().setAll(items);
}
});
Once that is done, you get the brown highlight on the selected row. You could of course easily support multiple brown highlights by replacing the int with a list of itns.
The best way I find to do this:
In my CSS
.table-row-cell:feederChecked{
-fx-background-color: #06FF00;
}
In my table initialization with a SimpleBooleanProperty of an Object content in my ObservableList:
// The pseudo classes feederChecked that were defined in the css file.
PseudoClass feederChecked = PseudoClass.getPseudoClass("feederChecked");
// Set a rowFactory for the table view.
tableView.setRowFactory(tableView -> {
TableRow<Feeder> row = new TableRow<>();
ChangeListener<Boolean> changeListener = (obs, oldFeeder, newFeeder) -> {
row.pseudoClassStateChanged(feederChecked, newFeeder);
};
row.itemProperty().addListener((obs, previousFeeder, currentFeeder) -> {
if (previousFeeder != null) {
previousFeeder.feederCheckedProperty().removeListener(changeListener);
}
if (currentFeeder != null) {
currentFeeder.feederCheckedProperty().addListener(changeListener);
row.pseudoClassStateChanged(feederChecked, currentFeeder.getFeederChecked());
} else {
row.pseudoClassStateChanged(feederChecked, false);
}
});
return row;
});
Code adapting from this complete exemple
I might have found something that works:
With this code added, if you press the button the highlighted row changes color, when you select a different row the color changes back to default, when you press the button again, it changes the color of the new row to brown.
final String css = getClass().getResource("style.css").toExternalForm();
final Scene scene = new Scene(new Group());
btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
scene.getStylesheets().add(css);
}
});
table.getSelectionModel().selectedIndexProperty()
.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
scene.getStylesheets().remove(css);
}
});
css:
.table-row-cell:selected
{
-fx-background-color: brown;
-fx-text-inner-color: white;
}
Only problem with this solution is that if you press the button twice in a row, your next row selected is already brown. You would have to use a seperate css file for this, else at startup of the application no css rules would be applied untill you press the button.
I found that the best solution would be to listen for row.itemProperty() changes because when you sort for example the rows change indexes, so rows get notified automatically.