JavaFX: CheckBoxTreeItem item not selected when checked - javafx

Before finding out about CheckBoxTreeItem, I used a regular TreeItem, with a ComboBox. The CellFactory had an onAction property which I passed into the CheckBox onAction method. The onAction EventHandler came from the Controller, so when I checked/unchecked that action method in the controller was called. The problem was the TreeItem did not get selected.
Now trying to use CheckBoxTreeItem instead. However I have a few problems with it regarding selection and action.
I need to perform an action when a CheckBox has been checked/unchecked. I want the checkbox to change state when the TreeItem has been selected. I need the TreeItem to be selected when a CheckBox is changed.
1)
Selecting the TreeItem does not change the selected state of the CheckBox. I had to write an onMousePressed action to do that.
#FXML public void performSelection() {
TreeItem<Mine> selectedItem = treeView.getSelectionModel().getSelectedItem();
boolean selected = ((CheckBoxTreeItem<Mine>) selectedItem).isSelected();
((CheckBoxTreeItem<Mine>) selectedItem).setSelected(!selected);
}
However, when a CheckBox on the TreeItem is checked/unchecked, that TreeItem is not selected. Is not that the point of CheckBoxTreeItem?
2)
I have created a ChangeListener that listens to the state of the CheckBoxTreeItem selectedProperty. This listener is registered on each items in the tree. It is the only way I can create an action to do some work on checked/unchecked.
However, checking an TreeItem, since it does not select the TreeItem I cannot access it with tree.getSelectionModel().getSelectedItem() in the listener. A CheckBox action where I cannot access the TreeItem data of that CheckBox's item is pointless. If I select a TreeItem it stays on that TreeItem, and if I check on a different TreeItem CheckBox, the selectedItem is Wrong.
If I select the top item and I want to perform some action on that items data, the change listener is called for ALL the children. This complicates matters. I only care of the state of the selected TreeItem which got checked/unchecked.
Is there no better way to add an action to the CheckBoxTreeItem than registering a ChangeListener?
3)
Uncheck the child will uncheck the parent, if there are no other siblings. I don't want that. However I want the children to be "gone"/unchecked if the parent is unchecked, but I don't want to be notified of it. If I uncheck and there are children, all the children should be unchecked. However I am not interesting in the change event of subitems of the unchecked item. My problem is still that all items below will fire a change event, and not just on the item I am interested in.
I could set each TreeItem independent. Then collapse the expanded item when it is unchecked. The children would still have its state independent from its parent checked or unchecked, but will not be visible.
Issue 1 are the most important problem I have. Checking the CheckBox needs to select the TreeItem of that CheckBox.
Issue 2 works with ChangeListener though I wish there was a better option, and the problem here can be circumvented by setting all items as independent.
Issue 3 I can also circumvent by setting all items to independent and hide them when parent is unchecked. They still maintain their state, but are out of sight.
A small test application
package com.company;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MineAppTest extends Application {
private TreeView<Mine> treeView;
private MineChangeListener mineChangeListener = new MineChangeListener();
public static void main(String[] argv) {
MineAppTest.launch();
}
/* (non-Javadoc)
* #see javafx.application.Application#start(javafx.stage.Stage)
*/
#Override
public void start(Stage primaryStage) throws Exception {
GridPane contentPane = new GridPane();
contentPane.getChildren().add(new Text("Hello World!"));
treeView = new TreeView<>();
treeView.setCellFactory(new MineTreeCellFactory());
treeView.setOnMousePressed(event -> {
TreeItem<Mine> selectedItem = treeView.getSelectionModel().getSelectedItem();
boolean selected = ((CheckBoxTreeItem<Mine>)selectedItem).isSelected();
((CheckBoxTreeItem<Mine>)selectedItem).setSelected(!selected);
selectedItem.setExpanded(!selected);
});
treeView.setShowRoot(false);
Mine mine1 = new Mine("mine", true);
CheckBoxTreeItem<Mine> rootItem = new CheckBoxTreeItem<>(mine1, null, true);
rootItem.setExpanded(true);
treeView.setRoot(rootItem);
Mine mine2 = new Mine("test1", true);
CheckBoxTreeItem<Mine> item1Level1 = new CheckBoxTreeItem<>(mine2, null, mine2.isEnabled());
item1Level1.setExpanded(true);
//item1Level1.setIndependent(true);
item1Level1.selectedProperty().addListener(mineChangeListener);
rootItem.getChildren().add(item1Level1);
Mine mine3 = new Mine("subtest1", true);
CheckBoxTreeItem<Mine> item1Level2 = new CheckBoxTreeItem<>(mine3, null, mine3.isEnabled());
item1Level2.setExpanded(true);
//item1Level2.setIndependent(true);
item1Level2.selectedProperty().addListener(mineChangeListener);
item1Level1.getChildren().add(item1Level2);
Mine mine4 = new Mine("subtest2", true);
CheckBoxTreeItem<Mine> item2Level2 = new CheckBoxTreeItem<>(mine4, null, mine4.isEnabled());
item2Level2.setExpanded(true);
//item2Level2.setIndependent(true);
item2Level2.selectedProperty().addListener(mineChangeListener);
item1Level1.getChildren().add(item2Level2);
Mine mine5 = new Mine("test2", true);
CheckBoxTreeItem<Mine> item2Level1 = new CheckBoxTreeItem<>(mine5, null, mine5.isEnabled());
item2Level1.setExpanded(true);
//item2Level1.setIndependent(true);
item2Level1.selectedProperty().addListener(mineChangeListener);
item1Level1.getChildren().add(item2Level1);
Mine mine6 = new Mine("subtest2", true);
CheckBoxTreeItem<Mine> item4Level2 = new CheckBoxTreeItem<>(mine6, null, mine6.isEnabled());
item4Level2.setExpanded(true);
//item4Level2.setIndependent(true);
item4Level2.selectedProperty().addListener(mineChangeListener);
item2Level1.getChildren().add(item4Level2);
Mine mine7 = new Mine("subtest3", true);
CheckBoxTreeItem<Mine> item5Level2 = new CheckBoxTreeItem<>(mine7, null, mine6.isEnabled());
item5Level2.setExpanded(true);
//item5Level2.setIndependent(true);
item5Level2.selectedProperty().addListener(mineChangeListener);
item4Level2.getChildren().add(item5Level2);
contentPane.getChildren().add(treeView);
primaryStage.setScene(new Scene(contentPane));
primaryStage.setWidth(200);
primaryStage.setHeight(400);
primaryStage.setTitle("JavaFX 8 app");
primaryStage.setOnCloseRequest(event -> Platform.exit());
primaryStage.show();
}
private class MineChangeListener implements ChangeListener<Boolean> {
/* (non-Javadoc)
* #see javafx.beans.value.ChangeListener#changed(javafx.beans.value.ObservableValue, java.lang.Object, java.lang.Object)
*/
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
CheckBoxTreeItem<Mine> item = (CheckBoxTreeItem<Mine>) treeView.getSelectionModel().getSelectedItem();
if (newValue) {
// Do some work on the data in the selected TreeItem
}
}
}
}
package com.company;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.util.Callback;
public class MineTreeCellFactory implements Callback<TreeView<Mine>, TreeCell<Mine>> {
/* (non-Javadoc)
* #see javafx.util.Callback#call(java.lang.Object)
*/
#Override
public TreeCell<Mine> call(TreeView<Mine> param) {
CheckBoxTreeCell<Mine> cell = new CheckBoxTreeCell<Mine>() {
/* (non-Javadoc)
* #see javafx.scene.control.Cell#updateItem(java.lang.Object, boolean)
*/
#Override
public void updateItem(Mine item, boolean empty) {
if (item == getItem() || (item != null && item.equals(getItem()))) {
return;
}
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (item != null) {
setText(item.getName());
} else {
setText(null);
setGraphic(null);
}
}
}
};
return cell;
}
}
package com.company;
import javafx.beans.property.SimpleBooleanProperty;
public class Mine {
private final String name;
private final SimpleBooleanProperty enabled;
public Mine(String name, boolean enabled) {
this.name = name;
this.enabled = new SimpleBooleanProperty(enabled);
}
/**
* #return the name
*/
public String getName() {
return name;
}
/**
* #return the description
*/
public String getDescription() {
return description;
}
public SimpleBooleanProperty enabledProperty() {
return enabled;
}
/**
* #return the isDisabled
*/
public boolean isEnabled() {
return enabled.get();
}
}

Related

Interacting with custom CellFactory node adds row to TableView selection list?

I have a TableView with a CellFactory that places a ComboBox into one of the columns. The TableView has SelectionMode.MULTIPLE enabled but it is acting odd with the ComboBox cell.
When the users clicks on the ComboBox to select a value, that row is added to the list of selected rows. Instead, clicking on the ComboBox should either select that row and deselect all others (unless CTRL is being held), or it should not select the row at all, but only allow for interaction with the ComboBox.
I am not sure how to achieve this.
Here is a complete example to demonstrate the issue:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
enum Manufacturer {
HP, DELL, LENOVO, ASUS, ACER;
}
public class TableViewSelectionIssue extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Simple TableView
TableView<ComputerPart> tableView = new TableView<>();
TableColumn<ComputerPart, Manufacturer> colManufacturer = new TableColumn<>("Manufacturer");
TableColumn<ComputerPart, String> colItem = new TableColumn<>("Item");
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colManufacturer.setCellValueFactory(t -> t.getValue().manufacturerProperty());
colItem.setCellValueFactory(t -> t.getValue().itemNameProperty());
tableView.getColumns().addAll(colManufacturer, colItem);
// CellFactory to display ComboBox in colManufacturer
colManufacturer.setCellFactory(param -> new ManufacturerTableCell(colManufacturer, FXCollections.observableArrayList(Manufacturer.values())));
// Add sample items
tableView.getItems().addAll(
new ComputerPart("Keyboard"),
new ComputerPart("Mouse"),
new ComputerPart("Monitor"),
new ComputerPart("Motherboard"),
new ComputerPart("Hard Drive")
);
root.getChildren().add(tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class ComputerPart {
private final ObjectProperty<Manufacturer> manufacturer = new SimpleObjectProperty<>();
private final StringProperty itemName = new SimpleStringProperty();
public ComputerPart(String itemName) {
this.itemName.set(itemName);
}
public Manufacturer getManufacturer() {
return manufacturer.get();
}
public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer.set(manufacturer);
}
public ObjectProperty<Manufacturer> manufacturerProperty() {
return manufacturer;
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
}
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
ManufacturerTableCell(TableColumn<ComputerPart, Manufacturer> column, ObservableList<Manufacturer> items) {
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
this.cboStatus.setConverter(new StringConverter<Manufacturer>() {
#Override
public String toString(Manufacturer object) {
return object.name();
}
#Override
public Manufacturer fromString(String string) {
return null;
}
});
this.cboStatus.disableProperty().bind(column.editableProperty().not());
this.cboStatus.setOnShowing(event -> {
final TableView<ComputerPart> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.cboStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (isEditing()) {
commitEdit(newValue);
column.getTableView().refresh();
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty) {
setGraphic(null);
} else {
this.cboStatus.setValue(item);
this.setGraphic(this.cboStatus);
}
}
}
The example begins with a predictable UI:
However, when interacting with the ComboBox in the Manufacturer column, the corresponding row is selected. This is expected for the first row, but it does not get deselected when interacting with another ComboBox.
How can I prevent subsequent interactions with a ComboBox from adding to the selected rows? It should behave like any other click on a TableRow, should it not?
I am using JDK 8u161.
Note: I understand there is a ComboBoxTableCell class available, but I've not been able to find any examples of how to use one properly; that is irrelevant to my question, though, unless the ComboBoxTableCell behaves differently.
Since you want an "always editing" cell, your implementation should behave more like CheckBoxTableCell than ComboBoxTableCell. The former bypasses the normal editing mechanism of the TableView. As a guess, I think it's your use of the normal editing mechanism that causes the selection issues—why exactly, I'm not sure.
Modifying your ManufactureTableCell to be more like CheckBoxTableCell, it'd look something like:
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
private final IntFunction<Property<Manufacturer>> extractor;
private Property<Manufacturer> property;
ManufacturerTableCell(IntFunction<Property<Manufacturer>> extractor, ObservableList<Manufacturer> items) {
this.extractor = extractor;
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
// removed StringConverter for brevity (accidentally)
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cboStatus.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isShortcutDown()) {
getTableView().getSelectionModel().select(getIndex(), getTableColumn());
} else {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
event.consume();
});
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
clearProperty();
if (empty) {
setGraphic(null);
} else {
property = extractor.apply(getIndex());
Bindings.bindBidirectional(cboStatus.valueProperty(), property);
setGraphic(cboStatus);
}
}
private void clearProperty() {
setGraphic(null);
if (property != null) {
Bindings.unbindBidirectional(cboStatus.valueProperty(), property);
}
}
}
And you'd install it like so:
// note you could probably share the same ObservableList between all cells
colManufacturer.setCellFactory(param ->
new ManufacturerTableCell(i -> tableView.getItems().get(i).manufacturerProperty(),
FXCollections.observableArrayList(Manufacturer.values())));
As already mentioned, the above implementation bypasses the normal editing mechanism; it ties the value of the ComboBox directly to the model item's property. The implementation also adds a MOUSE_PRESSED handler to the ComboBox that selects the row (or cell if using cell selection) as appropriate. Unfortunately, I'm not quite understanding how to implement selection when Shift is down so only "Press" and "Shortcut+Press" is handled.
The above works how I believe you want it to, but I could only test it out using JavaFX 12.

JavaFx Tableview checkbox requires focus

I implemented boolean representation in my tableView as checkbox. It works fine in general but very irritating fact is that it requires row to be focused (editing) to apply change of checkbox value. It means I first have to double click on the field and then click checkbox.
How to make checkbox change perform onEditCommit right away?
public class BooleanCell<T> extends TableCell<T, Boolean> {
private CheckBox checkBox;
public BooleanCell() {
checkBox = new CheckBox();
checkBox.selectedProperty().addListener(new ChangeListener<Boolean>() {
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (isEditing())
commitEdit(newValue == null ? false : newValue);
}
});
setAlignment(Pos.CENTER);
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
this.setEditable(true);
}
#Override
public void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
checkBox.setSelected(item);
setGraphic(checkBox);
}
}
}
I'm not sure about the rest of your implementation, but I assume you do not have your TableView set to editable:
tableView.setEditable(true);
On a side note, you could easily use a CheckBoxTableCell instead of implementing your own (BooleanCell).
Here is a very simple application you can run to see how this works. Each CheckBox may be clicked without first focusing the row and its value updates your underlying model as well (which you can see by clicking the "Print List" button).
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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.VBox;
import javafx.stage.Stage;
public class CheckBoxTableViewSample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// List of sample items
ObservableList<MyItem> myItems = FXCollections.observableArrayList();
myItems.addAll(
new MyItem(false, "Item 1"),
new MyItem(false, "Item 2"),
new MyItem(true, "Item 3"),
new MyItem(false, "Item 4"),
new MyItem(false, "Item 5")
);
// Create TableView
TableView<MyItem> tableView = new TableView<MyItem>();
// We need the TableView to be editable in order to allow each CheckBox to be selectable
tableView.setEditable(true);
// Create our table Columns
TableColumn<MyItem, Boolean> colSelected = new TableColumn<>("Selected");
TableColumn<MyItem, String> colName = new TableColumn<>("Name");
// Bind the columns with our model's properties
colSelected.setCellValueFactory(f -> f.getValue().selectedProperty());
colName.setCellValueFactory(f -> f.getValue().nameProperty());
// Set the CellFactory to use a CheckBoxTableCell
colSelected.setCellFactory(param -> {
return new CheckBoxTableCell<MyItem, Boolean>();
});
// Add our columns to the TableView
tableView.getColumns().addAll(colSelected, colName);
// Set our items to the TableView
tableView.setItems(myItems);
// Create a button to print out our list of items
Button btnPrint = new Button("Print List");
btnPrint.setOnAction(event -> {
System.out.println("-------------");
for (MyItem item : myItems) {
System.out.println(item.getName() + " = " + item.isSelected());
}
});
root.getChildren().addAll(tableView, btnPrint);
// Show the Stage
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
/**
* Just a simple sample class to display in our TableView
*/
final class MyItem {
// This property will be bound to the CheckBoxTableCell
private final BooleanProperty selected = new SimpleBooleanProperty();
// The name of our Item
private final StringProperty name = new SimpleStringProperty();
public MyItem(boolean selected, String name) {
this.selected.setValue(selected);
this.name.set(name);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
}

JavaFX Disable TableColumn based on checkbox state

Am looking to disable a TableColumn<CustomObject, String> tableColumn based on a field value in the CustomObject only when the TableColumn<CustomObject, Boolean> tableColumnTwo checkbox is checked. I can disable the textbox inside public void updateItem(String s, boolean empty) however not sure how to check the state of checkbox inside updateItem
Below is the relevant code snippet, would highly appreciate if anyone can shed light on this
#FXML
private TableColumn<CustomObject, Boolean> tableColumnTwo;
#FXML
private TableColumn<CustomObject, String> tableColumn;
tableColumn.setCellFactory(
new Callback<TableColumn<CustomObject, String>, TableCell<CustomObject, String>>() {
#Override
public TableCell<CustomObject, String> call(TableColumn<CustomObject, String> paramTableColumn) {
return new TextFieldTableCell<CustomObject, String>(new DefaultStringConverter()) {
#Override
public void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
TableRow<CustomObject> currentRow = getTableRow();
if(currentRow.getItem() != null && !empty) {
if (currentRow.getItem().getPetrified() == false) { // Need to check if checkbox is checked or not
setDisable(true);
setEditable(false);
this.setStyle("-fx-background-color: red");
} else {
setDisable(false);
setEditable(true);
setStyle("");
}
}
}
};
}
});
You can add a listener on the checkbox, which when checked will cause the table refresh.
data = FXCollections.observableArrayList(new Callback<CustomObject, Observable[]>() {
#Override
public Observable[] call(CustomObject param) {
return new Observable[]{param.petrifiedProperty()};
}
});
data.addListener(new ListChangeListener<CustomObject>() {
#Override
public void onChanged(ListChangeListener.Change<? extends CustomObject> c) {
while (c.next()) {
if (c.wasUpdated()) {
tableView.setItems(null);
tableView.layout();
tableView.setItems(FXCollections.observableList(data));
}
}
}
});
Your cellFactory would remain the same and would get called when a checkbox is checked/unchecked.
Usually, we expect cells being updated whenever they are notified about a change in the underlying data. To make certain that a notification is fired by the data on changing a property of an item, we need a list with an extractor on the properties that we are interested in, something like:
ObservableList<CustomObject> data = FXCollections.observableArrayList(
c -> new Observable[] {c.petrifiedProperty()}
);
With that in place the list fires a list change of type update whenever the pretified property changes.
Unfortunately, that's not enough due to a bug in fx: cells are not updated when receiving a listChange of type update from the underlying items. A dirty way around (read: don't use once the bug is fixed, it's using emergency api!) is to install a listener on the items and call table.refresh() when receiving an update.
An example:
import java.util.logging.Logger;
//import de.swingempire.fx.util.FXUtils;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;
/**
* CheckBoxTableCell: update editable state of one column based of
* the boolean in another column
* https://stackoverflow.com/q/46290417/203657
*
* Bug in skins: cell not updated on listChange.wasUpdated
*
* reported as
* https://bugs.openjdk.java.net/browse/JDK-8187665
*/
#SuppressWarnings({ "rawtypes", "unchecked" })
public class TableViewUpdateBug extends Application {
/**
* TableCell that updates state based on another value in the row.
*/
public static class DisableTextFieldTableCel extends TextFieldTableCell {
public DisableTextFieldTableCel() {
super(new DefaultStringConverter());
}
/**
* Just to see whether or not this is called on update notification
* from the items (it's not)
*/
#Override
public void updateIndex(int index) {
super.updateIndex(index);
// LOG.info("called? " + index);
}
/**
* Implemented to change background based on
* visible property of row item.
*/
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
TableRow<TableColumn> currentRow = getTableRow();
boolean editable = false;
if (!empty && currentRow != null) {
TableColumn column = currentRow.getItem();
if (column != null) {
editable = column.isVisible();
}
}
if (!empty) {
setDisable(!editable);
setEditable(editable);
if (editable) {
this.setStyle("-fx-background-color: red");
} else {
this.setStyle("-fx-background-color: green");
}
} else {
setStyle("-fx-background-color: null");
}
}
}
#Override
public void start(Stage primaryStage) {
// data: list of tableColumns with extractor on visible property
ObservableList<TableColumn> data = FXCollections.observableArrayList(
c -> new Observable[] {c.visibleProperty()});
data.addAll(new TableColumn("first"), new TableColumn("second"));
TableView<TableColumn> table = new TableView<>(data);
table.setEditable(true);
// hack-around: call refresh
data.addListener((ListChangeListener) c -> {
boolean wasUpdated = false;
boolean otherChange = false;
while(c.next()) {
if (c.wasUpdated()) {
wasUpdated = true;
} else {
otherChange = true;
}
}
if (wasUpdated && !otherChange) {
table.refresh();
}
//FXUtils.prettyPrint(c);
});
TableColumn<TableColumn, String> text = new TableColumn<>("Text");
text.setCellFactory(c -> new DisableTextFieldTableCel());
text.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<TableColumn, Boolean> visible = new TableColumn<>("Visible");
visible.setCellValueFactory(new PropertyValueFactory<>("visible"));
visible.setCellFactory(CheckBoxTableCell.forTableColumn(visible));
table.getColumns().addAll(text, visible);
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root, 300, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableViewUpdateBug.class.getName());
}

Apply css on specific rows on javaFX

I'm using a TreeTableView in my project, and I would like to do something specific when the user selects a row:
I would like this row to have a different background color, but i would also like its childs and parents to have this color too.
I found a way to access every rows and children, but I just don't know how to specify this background color. I tried to do this using my customs TreeTableCells and adding the style in my updateItem method, but this method is not called each time an item is selected.
So i wanted to try to add listener in my treetableview, which seems to be a better idea, but in fact i'm not able to access the rows to give them any style.
The basic strategy here is:
Create CSS PseudoClass instances for the conditions you want to highlight (in the example below I created one for "child of selected" and one for "parent of selected")
Use a rowFactory to create rows for the table. The rows should update their pseudoclass state in the updateItem method, and should also update their pseudoclass state if the selected items change. You can place a listener on the table's selected items to do the second of these.
Add CSS in the external CSS file to style the rows the way you want, using the pseudoclasses you defined in the first step.
Here is a SSCCE:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectedPath extends Application {
private PseudoClass childOfSelected = PseudoClass.getPseudoClass("child-of-selected");
private PseudoClass parentOfSelected = PseudoClass.getPseudoClass("parent-of-selected");
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<>(createRandomTree(50));
table.setRowFactory(ttv -> {
TreeTableRow<Item> row = new TreeTableRow<Item>() {
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
pseudoClassStateChanged(parentOfSelected, false);
pseudoClassStateChanged(childOfSelected, false);
} else {
updateState(this);
}
}
};
table.getSelectionModel().getSelectedItems().addListener(
(Change<? extends TreeItem<Item>> c) -> updateState(row));
return row ;
});
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Scene scene = new Scene(table, 800, 800);
scene.getStylesheets().add("table-row-highlight.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private <T> void updateState(TreeTableRow<T> row) {
TreeTableView<T> table = row.getTreeTableView() ;
TreeItem<T> item = row.getTreeItem();
// if item is selected, just use default "selected" highlight,
// and set "child-of-selected" and "parent-of-selected" to false:
if (item == null || table.getSelectionModel().getSelectedItems().contains(item)) {
row.pseudoClassStateChanged(childOfSelected, false);
row.pseudoClassStateChanged(parentOfSelected, false);
return ;
}
// check to see if item is parent of any selected item:
for (TreeItem<T> selectedItem : table.getSelectionModel().getSelectedItems()) {
for (TreeItem<T> parent = selectedItem.getParent(); parent != null ; parent = parent.getParent()) {
if (parent == item) {
row.pseudoClassStateChanged(parentOfSelected, true);
row.pseudoClassStateChanged(childOfSelected, false);
return ;
}
}
}
// check to see if item is child of any selected item:
for (TreeItem<T> ancestor = item.getParent() ; ancestor != null ; ancestor = ancestor.getParent()) {
if (table.getSelectionModel().getSelectedItems().contains(ancestor)) {
row.pseudoClassStateChanged(childOfSelected, true);
row.pseudoClassStateChanged(parentOfSelected, false);
return ;
}
}
// if we got this far, clear both pseudoclasses:
row.pseudoClassStateChanged(childOfSelected, false);
row.pseudoClassStateChanged(parentOfSelected, false);
}
private <S,T> TreeTableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TreeTableColumn<S,T> column = new TreeTableColumn<>(title);
column.setCellValueFactory(cellData -> property.apply(cellData.getValue().getValue()));
return column ;
}
private TreeItem<Item> createRandomTree(int numNodes) {
Random rng = new Random();
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", rng.nextInt(1000)));
root.setExpanded(true);
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= numNodes; i++) {
Item item = new Item("Item "+i, rng.nextInt(1000));
TreeItem<Item> treeItem = new TreeItem<>(item);
treeItem.setExpanded(true);
items.get(rng.nextInt(items.size())).getChildren().add(treeItem);
items.add(treeItem);
}
return root ;
}
public static class Item {
private StringProperty name = new SimpleStringProperty();
private IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
#Override
public String toString() {
return String.format("%s (%d)", getName(), getValue());
}
}
public static void main(String[] args) {
launch(args);
}
}
and the CSS file (table-row-highlight.css):
.tree-table-row-cell:child-of-selected {
-fx-background: green ;
}
.tree-table-row-cell:parent-of-selected {
-fx-background: salmon ;
}
This give the following:
This version highlights all descendant nodes and all ancestor nodes of the selected items in the tree. You can simplify the updateState() method if you only want immediate child and parent rows highlighted.

Change TableCell style on focus without setCellSelectionEnabled

Is there a way to style a TableCell in a TableView without tableView.getSelectionModel().setCellSelectionEnabled(true); in JavaFX?
I tried this solution https://community.oracle.com/thread/3528543?start=0&tstart=0 but it randomly fails to highlight the row
ex:
tableView.getSelectionModel().setCellSelectionEnabled(true);
final ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();
final PseudoClass selectedRowPseudoClass = PseudoClass.getPseudoClass("selected-row");
tableView.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {
selectedRowIndexes.clear();
tableView.getSelectionModel().getSelectedCells().stream().map(TablePosition::getRow).forEach(row -> {
selectedRowIndexes.add(row);
});
});
tableView.setRowFactory(tableView -> {
final TableRow<List<StringProperty>> row = new TableRow<>();
BooleanBinding selectedRow = Bindings.createBooleanBinding(() ->
selectedRowIndexes.contains(row.getIndex()), row.indexProperty(), selectedRowIndexes);
selectedRow.addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(selectedRowPseudoClass, newValue);
}
);
return row;
});
Okay. So as long as your cell actually gets the focus, providing a custom TableCell to the cell factory of the column you want to style differently will allow you to listen to any property of the TableCell, since you are defining that TableCell yourself. Below is an example on how to listen to the focusedProperty of the TableCell and change the style when that happens.
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.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class MCVE3 extends Application {
#Override
public void start(Stage stage) {
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// I have no idea how to get focus on a cell unless you enable cell selection. It does not seem to be possible at all.
table.getSelectionModel().setCellSelectionEnabled(true);
// Initializes a column and adds it to the table.
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>("Column");
col.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().get(0)));
// Initializes a column and adds it to the table.
TableColumn<ObservableList<String>, String> col2 = new TableColumn<ObservableList<String>, String>("Column 2");
col2.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().get(1)));
// We add a custom cell factory to second column. This enables us to customize the behaviour of the cell.
col2.setCellFactory(e -> new FocusStyleCell());
table.getColumns().addAll(col, col2);
// Add data to the table.
table.getItems().add(FXCollections.observableArrayList("One", "OneTwo"));
table.getItems().add(FXCollections.observableArrayList("Two", "TwoTwo"));
table.getItems().add(FXCollections.observableArrayList("Three", "ThreeTwo"));
table.getItems().add(FXCollections.observableArrayList("Four", "FourTwo"));
BorderPane view = new BorderPane();
view.setCenter(table);
stage.setScene(new Scene(view));
stage.show();
}
/**
* A custom TableCell that will change style on focus.
*/
class FocusStyleCell extends TableCell<ObservableList<String>, String> {
// You always need to override updateItem. It's very important that you don't forget to call super.updateItem when you do this.
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(item);
}
}
public FocusStyleCell() {
// We add a listener to the focusedProperty. newValue will be true when the cell gets focused.
focusedProperty().addListener((obs, oldValue, newValue) -> {
if (newValue) {
setStyle("-fx-background-color: black;");
// // Or add some custom style class:
// if (getStyleClass().contains("focused-cell")) {
// getStyleClass().add("focused-cell");
// }
} else {
// If you instead wish to use style classes you need to
// remove that style class once focus is lost.
// getStyleClass().remove("focused-cell");
setStyle("-fx-background-color: -fx-background");
}
});
}
}
public static void main(String[] args) {
launch();
}
}
I could not solve it using focus listener but it is possible using a MouseEvent.MOUSE_CLICKED
column.setCellFactory(column1 -> {
TableCell<List<StringProperty>, String> cell = new TextFieldTableCell<>();
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, e ->
cell.setStyle("-fx-border-color:black black black black;-fx-background-color:#005BD1;-fx-text-fill:white")
);
cell.addEventFilter(MouseEvent.MOUSE_EXITED, e ->
cell.setStyle("")
);
return cell;
});
Full example https://github.com/gadelkareem/aws-client/blob/master/src/main/java/com/gadelkareem/awsclient/application/Controller.java#L443

Resources