I would like to drag and drop an item from a treeview into a textArea.
The goal is to write in the textArea the item dropped.
So if i choose to drop the item b, I would like to have b written in the textArea.
I have two problems.
1°) I have an error
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Only serializable objects or ByteBuffer can be used as data with data format []
2°) My setOn events don't run at all.
At any time, an event is detected.
package application;
import java.io.Serializable;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class testDragAndDropTreeView extends Application implements Serializable {
private final TreeView<String> tree = new TreeView<String>();
private final TextArea text = new TextArea();
private static final DataFormat DATAFORMAT = new DataFormat();
#Override
public void start(Stage Stage) {
HBox hbox = new HBox();
hbox.setPadding(new Insets(20));
TreeItem<String> a = new TreeItem<String>("a");
TreeItem<String> b = new TreeItem<String>("b");
TreeItem<String> c = new TreeItem<String>("c");
TreeItem<String> root = new TreeItem<String>("Root");
root.getChildren().addAll(a, b, c);
root.setExpanded(true);
tree.setRoot(root);
tree.setOnDragDetected(event -> {
Dragboard db = tree.startDragAndDrop(TransferMode.ANY);
ClipboardContent content = new ClipboardContent();
content.put(DATAFORMAT, tree.getSelectionModel().getSelectedItem());
db.setContent(content);
});
text.setOnDragOver(event -> {
if (event.getGestureSource() != text && event.getDragboard().hasContent(DATAFORMAT)) {
event.acceptTransferModes(TransferMode.ANY);
}
event.consume();
});
text.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasContent(DATAFORMAT)) {
Person droppedPerson = (Person) db.getContent(DATAFORMAT);
text.appendText(droppedPerson.getName() + "\n");
success = true;
}
event.setDropCompleted(success);
event.consume();
});
tree.setCellFactory(param -> {
return new TreeCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setStyle(null);
setGraphic(null);
setText(null);
return;
}
setText(item);
}
};
});
hbox.getChildren().addAll(tree, text);
Stage.setScene(new Scene(hbox));
Stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
A few of observations:
You were trying to put TreeItems in the clipboard content, but they aren't serializable and are not of the expected type, so they don't fit. You actually wanted to put the value of the tree items in the clipboard (I think).
You can just use a map to transfer data (clipboard content is also a map, so I guess you could still use that if you wanted).
You just want strings of data, so you should use DataFormat.PLAIN_TEXT and put a string in the transfer object.
You should cater for what to do if nothing is selected (selected item is null).
Your code was referencing a non-existent class Person, I just changed it to String.
The action of initiating a drag from a tree item selects the tree item before the drag occurs (this is the default operation of the tree with the mouse events you are consuming, not additional code).
The action of initiating a drag from a blank cell below the visible tree items, will drag and drop the selected item (if there is one or nothing at all), which is a bit strange but sounds like what you want to occur from comments.
An alternative would be to define drag handlers on the tree cells themselves rather than the entire tree, but that differs a bit from the design you seem to have.
Your code wasn't consistently following camel case naming conventions for Java, so I fixed some names. It is especially a bad idea to name a Stage variable Stage exactly the same case as the class name, as that is really confusing.
The root is shown and draggable. If you don't want the root shown, you can call tree.setShowRoot(false).
Example
Test this modification of your code and verify if it does what you want. If it is not exactly what you want, perhaps you can use it as a new starting point and tweak it how you want.
import java.io.Serializable;
import java.util.Map;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class DragAndDropTreeViewApp extends Application implements Serializable {
private static final DataFormat DATA_FORMAT = DataFormat.PLAIN_TEXT;
private final TreeView<String> tree = new TreeView<>();
private final TextArea text = new TextArea();
#Override
public void start(Stage stage) {
HBox hbox = new HBox();
hbox.setPadding(new Insets(20));
TreeItem<String> a = new TreeItem<>("a");
TreeItem<String> b = new TreeItem<>("b");
TreeItem<String> c = new TreeItem<>("c");
TreeItem<String> root = new TreeItem<>("Root");
//noinspection unchecked
root.getChildren().addAll(a, b, c);
root.setExpanded(true);
tree.setRoot(root);
tree.setOnDragDetected(event -> {
if (tree.getSelectionModel().getSelectedItem() != null) {
Dragboard db = tree.startDragAndDrop(TransferMode.ANY);
db.setContent(
Map.of(
DATA_FORMAT,
tree.getSelectionModel().getSelectedItem().getValue()
)
);
}
});
text.setOnDragOver(event -> {
if (event.getGestureSource() != text && event.getDragboard().hasContent(DATA_FORMAT)) {
event.acceptTransferModes(TransferMode.ANY);
}
event.consume();
});
text.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasContent(DATA_FORMAT)) {
String droppedPerson = (String) db.getContent(DATA_FORMAT);
text.appendText(droppedPerson + "\n");
success = true;
}
event.setDropCompleted(success);
event.consume();
});
tree.setCellFactory(param -> new TreeCell<>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setStyle(null);
setGraphic(null);
setText(null);
return;
}
setText(item);
}
});
hbox.getChildren().addAll(tree, text);
stage.setScene(new Scene(hbox));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Related
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.
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());
}
I have a ListView which is a dragdrop target. When handling OnDragDropped event, I would like to find the list cell which is at the position of the mouse. In addition I want to highlight items when mouse is hovered above them even during a drag drop operation. How can this be achieved in JavaFx.
Use a cell factory on the list view to define custom cells: that way you can register the drag handlers with the individual cells, instead of with the list view. Then it is easy for the drag handlers to know which cells have fired the event.
Here is a SSCCE:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class ListViewWithDragAndDrop extends Application {
#Override
public void start(Stage primaryStage) {
ListView<String> listView = new ListView<>();
listView.getItems().addAll("One", "Two", "Three", "Four");
listView.setCellFactory(lv -> {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty) ;
setText(item);
}
};
cell.setOnDragOver(e -> {
Dragboard db = e.getDragboard();
if (db.hasString()) {
e.acceptTransferModes(TransferMode.COPY);
}
});
cell.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
if (db.hasString()) {
String data = db.getString();
if (cell.isEmpty()) {
System.out.println("Drop on empty cell: append data");
listView.getItems().add(data);
} else {
System.out.println("Drop on "+cell.getItem()+": replace data");
int index = cell.getIndex();
listView.getItems().set(index, data);
}
e.setDropCompleted(true);
}
});
// highlight cells when drag target. In real life, use an external CSS file
// and CSS pseudoclasses....
cell.setOnDragEntered(e -> cell.setStyle("-fx-background-color: gold;"));
cell.setOnDragExited(e -> cell.setStyle(""));
return cell ;
});
TextField textField = new TextField();
textField.setPromptText("Type text and drag to list view");
textField.setOnDragDetected(e -> {
Dragboard db = textField.startDragAndDrop(TransferMode.COPY);
String data = textField.getText();
Text text = new Text(data);
db.setDragView(text.snapshot(null, null));
ClipboardContent cc = new ClipboardContent();
cc.putString(data);
db.setContent(cc);
});
BorderPane root = new BorderPane(listView, textField, null, null, null);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
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.
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