Get TableView cell data using MouseEvent - javafx

I currently have a TableView with a custom CellFactory. I am trying to figure out how to get the core data underlying a cell when the mouse enters that cell.
I will be using this data to populate a label that is defined in another controller.
I've been trying to look at event filters and event handlers, and suspect the answer has something to do with using those appropriately, but I haven't been able to figure this out or find an answer.
Please let me know what code, if any, you would like to see. I am not even sure what would help at this point.

Register a mouse listener with the cell to handle mouseEntered events, and call getItem() on the cell to get the data:
TableView<MyRowType> table = ... ;
TableColumn<MyRowType, MyCellType> column = ... ;
column.setCellFactory ( c-> {
TableCell<MyRowType, MyCellType> cell = new TableCell<MyRowType, MyCellType>() {
#Override
public void updateItem(MyCellType item, boolean empty) {
super.updateItem(item, empty) ;
// your implementation here...
}
};
cell.setOnMouseEntered( e -> {
MyCellType item = cell.getItem();
if (item != null) {
// do whatever you need with item
}
});
return cell ;
});

Related

TableView modify style per cell only [duplicate]

This question already has an answer here:
How to get row value inside updateItem() of CellFactory
(1 answer)
Closed 4 years ago.
Assume a:
TableView<ResultType, String> table = new TableView<> ();
table.setItems(myItems);
In TableColumn we have the setCellValueFactory method which very nicely gives you access to the ResultType object of the respective cell value. So you can use it to extract the values like that:
aColumnFromTableView.setCellValueFactory(data -> new SimpleStringProperty(data.getValue));
Now each cell from aColumnFromTableView will be populated with a value from all ResultType objects which are set as items for the table.
The question is: can we also change the cell's style in a similar way? I had a look at the setCellFactory method, but it does not seem as friendly as setCellValueFactory though (= it does not provide me the respective ResultType).
Here's what you can do with setCellFactory:
aColumnFromTableView.setCellValueFactory(data -> ???? ); // data is actually aColumnFromTableView itself??
So I am wondering of a way to set the cell style individually similar to what I described with "setCellValueFactory". I hope it exists.
Note: I also tried
aColumnFromTableView.setCellValueFactory(data -> {
aColumnFromTableView.setStyle("my style");
return new SimpleStringProperty(data.getValue);
})
But that sets it for the entire column and not individually.
Thanks!!!!
If you want to customize the style of the TableCell based on the value of the cell you'll need to use a cellFactory and return your own TableCell.
For instance, if you wanted a TableCell<?, Double> that displayed the number in red if it was negative you could do:
column.setCellFactory(col -> new TableCell<>() {
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.toString());
if (item < 0.0) {
setTextFill(Color.RED); // or use setStyle(String)
} else {
setTextFill(Color.BLACK); // or use setStyle(String)
}
}
}
});
When creating a custom TableCell you'll more than likely want to override the updateItem(Object,boolean) method. It's important you override it correctly, however, if you want it to work right. Read the javadoc for information:
The updateItem method should not be called by developers, but it is the best method for developers to override to allow for them to customise the visuals of the cell. To clarify, developers should never call this method in their code (they should leave it up to the UI control, such as the ListView control) to call this method. However, the purpose of having the updateItem method is so that developers, when specifying custom cell factories (again, like the ListView cell factory), the updateItem method can be overridden to allow for complete customisation of the cell.
It is very important that subclasses of Cell override the updateItem method properly, as failure to do so will lead to issues such as blank cells or cells with unexpected content appearing within them. Here is an example of how to properly override the updateItem method:
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.toString());
}
}
Note in this code sample two important points:
We call the super.updateItem(T, boolean) method. If this is not done, the item and empty properties are not correctly set, and you are likely to end up with graphical issues.
We test for the empty condition, and if true, we set the text and graphic properties to null. If we do not do this, it is almost guaranteed that end users will see graphical artifacts in cells unexpectedly.
Instead of setting properties or calling setStyle you could use things like PseudoClass states to make it easier to style from an external CSS stylesheet.
import javafx.css.PseudoClass;
import javafx.scene.control.TableCell;
public class CustomCell<S> extends TableCell<S, Double> {
private static final PseudoClass POSITIVE = PseudoClass.getPseudoClass("positive");
private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative");
public CustomCell() {
getStyleClass().add("custom-cell");
}
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
pseudoClassStateChanged(POSITIVE, false);
pseudoClassStateChanged(NEGATIVE, false);
} else {
setText(item.toString()); // you might want to format the number for display
pseudoClassStateChanged(POSITIVE, item >= 0.0);
pseudoClassStateChanged(NEGATIVE, item < 0.0);
}
}
}
Then use:
column.setCellFactory(col -> new CustomCell<>());
And in a stylesheet:
.custom-cell:positive {
-fx-text-fill: black;
}
.custom-cell:negative {
-fx-text-fill: red;
}

JavaFX TreeTableView set custom node in row cell

I want to create a treetableview which can show for every tree item a sub tree with a row which contains custom nodes (in this case a button).
In the end i want to make it look like that.
In order to achieve that i probably need a custom row factory.
//set custom row factory
treeTableView.setRowFactory(new Callback<TreeTableView<Struct>, TreeTableRow<Struct>>() {
#Override
public TreeTableRow<Struct> call(TreeTableView<Struct> param) {
return new TreeTableRow<Struct>(){
#Override
protected void updateItem(Struct item, boolean empty) {
super.updateItem(item, empty);
//check if row has values
if (!empty) {
//override the row cell graphics if boolean is set
if (item.isCustomRow()) {
setGraphic(new StackPane(new Button("Button")));
}
} else {
setGraphic(null);
}
}
};
}
});
My idea was to override the updateItem() method and use the setGraphic() method to display the button. To indicate whether the tree item shows my own graphic node or the default, i added a boolean to my tableview structure class (Struct).
//Struct contains treetable data
class Struct {
...
//the boolean defines if the row graphics should be overridden
Boolean customRow;
...
public Boolean isCustomRow() {
return customRow;
}}
The result which i got can you see here. I looks like the empty column cells inside the row blocks my own node.
I suspect that i also need a custom TreeTableRowSkin but i didnt find any examples how to implement this. Is there anyone who can help me to solve this?
The full code example can be found here.

JavaFX - using setRowFactory to highlight new rows

I am writing a JavaFX app where a series of messages appear in a TableView. When a new message appears, its row in the table should be highlighted, meaning its background color should be orange or something. Once the user clicks it, the background color should clear, acknowledging the message was read. Should be simple.
I've done enough research to realize that I need to use a rowFactory to set or clear a row's background. But I'm struggling with the mechanics of setRowFactory(). The documentation on Oracle is over my head, and every example I pull up online seems radically different than the last one.
Here's what I have:
public class Message {
private boolean readOnce;
private int date;
private String msg;
public Message(int date, String msg, String msg2){
this.readOnce = false;
this.date = date;
this.msg = msg;
}
public boolean isReadOnce() {
return readOnce;
}
public void setReadOnce(){
readOnce = true;
}
// ...and more standard getters & setters here...
}
The TableView is set up in the main controller:
#FXML TableView<Message> messageTable;
#FXML TableColumn<Message, Integer> Col1;
#FXML TableColumn<Message, String> Col2;
ObservableList<Message> tableItems;
// ...
// Setting up the Table:
PropertyValueFactory<Message, Integer> dateProperty = new PropertyValueFactory<Message, Integer>("date");
PropertyValueFactory<Message, String> msgProperty = new PropertyValueFactory<Message, String>("msg");
Col1.setCellValueFactory( dateProperty );
Col2.setCellValueFactory( msgProperty );
messageTable.setItems( tableItems );
// If we click an item in the table: messageTable.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if (newSelection != null) {
System.out.println("Troubleshoot: You clicked: "+newSelection.getMsg());
newSelection.setReadOnce(true);
}
});
And if I want to add a new message to the table, I just add it into the observable list:
public void addMsg(int num, String msg){
tableItems.add(new Message(num, msg));
}
So far, pretty easy. But I'm all thumbs when it comes to the rowFactory:
messageTable.setRowFactory(messageTable -> {
TableRow<Message> row = new TableRow<>();
ObjectProperty<Message> opMsg = row.itemProperty();
Message tmpMsg = opMsg.get();
if(!tmpMsg.isReadOnce()){
row.getStyleClass().add("highlight-message"); // defined in CSS
} else {
row.getStyleClass().add("clear-message"); // defined in CSS
}
return row;
});
To be very honest, I have no idea what I'm doing here. I understand that the rowFactory takes in the entire table and regenerates each row one-by-one. What I don't understand is how does the RowFactory code examine each Message in the table and how can I access them? Originally I thought these line might allow me to see the Message within the row:
TableRow<Message> row = new TableRow<>();
ObjectProperty<Message> opMsg = row.itemProperty();
Message tmpMsg = opMsg.get();
But when I debug the code, tmpMsg == NULL. So that's a big fat dead end.
Anyone see what I'm doing wrong? I've been researching this for about a week, getting absolutely no-where. Any help anyone can offer is wildly appreciated.
Many thanks,
-RAO
TableRows are created by TableView to fill it's viewport and contain TableCells. At the time they are created the item property still contains the default value null. You could register a listener to that property but usually I prefer overriding the updateItem method of a cell.
Also using PseudoClass is simpler than using style classes. New items can be assigned to a row; this could result in the same style class being added multiple times and even both style classes could be added to the same cell. PseudoClasses however can be switched on/of without the need to take care of removing other classes.
final PseudoClass highlightMessage = PseudoClass.getPseudoClass("highlight-message");
messageTable.setRowFactory(messageTable -> new TableRow<Message>() {
{
selectedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
Message item = getItem();
if (item != null) {
item.setReadOnce();
pseudoClassStateChanged(highlightMessage, false);
}
}
});
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
pseudoClassStateChanged(highlightMessage, item != null && !item.isReadOnce());
}
});
In a CSS stylesheet you could use rules like this:
.table-row-cell:filled {
/* style for non-highlighted rows */
}
.table-row-cell:filled:highlight-message {
/* style for highlighted rows */
}
Note that this does not allow you to programmatically alter the read state. It updates the state on selecting a cell. You could add a BooleanProperty to Message or use a ObservableSet to store the highlighted messages and update the state of cells from a listener if you need to programmatically update the readOnce property. In the latter case you do not need to store a readOnce property in the Message itself...

Javafx : ComboBoxTableCell - how to select a value in one click?

I have a TableView with a ComboBoxTableCell, when using the default implementation the user have to click three times to select a value from of the ComboBox's list.
I want when the user clicks on the cell to show the combo box list. I based my solution on this one:
JavaFX editable ComboBox in a table view
The cell does get into edit mode (startEdit() is called) but it takes another click to show the list of values, what am I missing?
table.addEventHandler(MouseEvent.MOUSE_CLICKED, (e) ->
{
if (table.getEditingCell() == null)
{
TablePosition focusedCellPos = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPos.getRow(), focusedCellPos.getTableColumn());
}
});
Thanks.
Interesting problem - bubbling up again after quite a while :)
Looks like the approach of the OP is indeed working (as of fx11, some bugs around its editing seem to be fixed) - with a little help from the combo cell:
start editing in a single click handler on the tableView (from OP)
extend ComboBoxTableCell and override its startEdit to open the dropDown
Code snippet:
// set editable to see the combo
table.setEditable(true);
// keep approach by OP
table.addEventHandler(MouseEvent.MOUSE_CLICKED, (e) -> {
TablePosition<Person, ?> focusedCellPos = table.getFocusModel()
.getFocusedCell();
if (table.getEditingCell() == null) {
table.edit(focusedCellPos.getRow(),
focusedCellPos.getTableColumn());
}
});
// use modified standard combo cell shows its popup on startEdit
firstName.setCellFactory(cb -> new ComboBoxTableCell<>(firstNames) {
#Override
public void startEdit() {
super.startEdit();
if (isEditing() && getGraphic() instanceof ComboBox) {
// needs focus for proper working of esc/enter
getGraphic().requestFocus();
((ComboBox<?>) getGraphic()).show();
}
}
});
Maybe not the cleanest solution to this problem, but I found a workaround to make the ComboBoxTableCells drop down its menu in just 1 click:
column.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
#Override
public TableCell<Person, String> call(TableColumn<Person, String> column) {
ComboBoxTableCell cbtCell = new ComboBoxTableCell<>(cbValues);
cbtCell.setOnMouseEntered(new EventHandler<Event>() {
#Override
public void handle(Event event) {
// Without a Person object, a combobox shouldn't open in that row
if (((Person)((TableRow)cbtCell.getParent()).getItem()) != null) {
Robot r = new Robot();
r.mouseClick(MouseButton.PRIMARY);
r.mouseClick(MouseButton.PRIMARY);
}
}
});
return cbtCell;
}
});
PS: I know that this topic is a bit old, but I also stumbled upon this problem recently and could not find any working solution to it online. As I sad, it's not the cleanest workaround, but at least it does its job. ;)

ChoiceBox inside a TableView's cell only through custom Callback

I have been using a custom cell factory to display a ChoiceBox inside a TableView's TableColumn's cell, i.e.
someColumn.setCellFactory(new DayCellFactory());
with DayCellFactory implementing Callback (with the call and updateItem methods), and the ChoiceBox's (and thus the cell's) value being bound to the value of a property. The ChoiceBox shows the potential values of the enum Day.
class DayCellFactory implements Callback<TableColumn<DayPeriod, Day>, TableCell<DayPeriod, Day>> {
private final static ObservableList<Day> list = FXCollections.observableArrayList();
static {
for (Day day : Day.values()) {
list.add(day);
}
}
#Override
public TableCell<DayPeriod, Day> call (TableColumn<DayPeriod, Day> column) {
return new TableCell<DayPeriod, Day>() {
#Override
public void updateItem (Day item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
ObservableValue<Day> obsVal = getTableColumn().getCellObservableValue(getIndex());
ObjectProperty<Day> objProp = (ObjectProperty<Day>) obsVal;
ChoiceBox<Day> choice = new ChoiceBox<>(list);
choice.valueProperty().bindBidirectional(objProp);
setGraphic(choice);
}
}
};
}
}
I thought I could reduce the code by simply using
someColumn.setCellFactory(ChoiceBoxTableCell.forTableColumn(list));
(with the list defined and filled as required) but the resulting cell looks (and behaves) like a simple label showing the property's toString() value - yes, I did click there, but no ChoiceBox appears. And removing this line makes no difference. Removing the cell value factory results in no value being displayed.
I guess that there is no binding taking place with this simple approach, but this can probably be taken care of as well. It just seems strange that there is no ChoiceBox appearing.

Resources