I have a list view with the following cell factory:
availableSymbolsTable.setCellFactory(lv -> {
ListCell<Symbol> cell = new ListCell<Symbol>() {
#Override
protected void updateItem(Symbol t, boolean empty) {
super.updateItem(t, empty);
if (empty) {
setText(null);
} else {
setText(t.getSymbolName());
}
}
};
cell.setOnKeyPressed(e ->
{
//This never fires
}
);
cell.setOnMouseEntered(e -> {
//This works
});
cell.setOnMouseClicked(e -> {
if (cell.getItem() != null) {
if(e.getClickCount() == 2)
{
//This works
}
}
});
return cell ;
} );
I have added 3 event handles in the same manner. OnMouseEntered and OnMouseClicked both work as expected. However, OnKeyPressed is never executed. The same goes for OnKeyReleased. When pressing arrow keys, the listview changes the selected row as expected, but my event handler code is never executed. What seems to be the problem?
Related
I want to add/remove a style class to a table row based on a boolean in the row item.
Adding and removing the class works as expected with the following code. But when I click on the column header to reorder the table, the style sticks to the row id instead of the row item. Meaning if before ordering the first row was styled, after ordering the style is still on the first row instead of the row at the new position.
setRowFactory(table -> {
TableRow<PowerPlantPM> row = new TableRow<>() {
#Override
protected void updateItem(PowerPlantPM pp, boolean empty) {
super.updateItem(pp, empty);
if (!empty && pp != null) {
pp.savedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
getStyleClass().remove("unsaved");
} else {
getStyleClass().add("unsaved");
}
});
// the following binding works (including ordering), but is not what I want because of the ":selected" pseudo class
// styleProperty().bind(Bindings.when(pp.savedProperty()).then("").otherwise("-fx-background-color: #f2dede"));
}
}
};
return row;
});
I hope it is clear what I want to achieve. How do I get the style to stick to the row item when reordering?
A TableRow is reused as much as possible, in your updateItem you need to query the corresponding property, not add a listener to it. The listener will only fire if the property changes, but the TableRow may asked to redraw on a different position, or a different item.
protected void updateItem(PowerPlantPM pp, boolean empty) {
super.updateItem(pp, empty);
if (!empty && pp != null) {
if (!pp.isSaved()) {
getStyleClass().add("unsaved");
} else {
getStyleClass().remove("unsaved");
}
.....
}
}
Create your ObservableList with the properties it should watch with
ObservableListFX<PowerPlantPM> powerplants =
Collections.observableArrayList(pp -> new Observable[] { pp.savedProperty() });
This list will report changes on the items for the properties you returned in the Observable[].
You never unregister the listener from the old items. Also the listener is not called for the initial value of the property. Even if it was, your code could result in the same style class being added multiple times to a node. Furthermore cells may become empty. You need to remove the style class in that case too.
To avoid adding the same style class multiple times use a pseudoclass:
final PseudoClass unsaved = PseudoClass.getPseudoClass("unsaved");
setRowFactory(table -> {
TableRow<PowerPlantPM> row = new TableRow<>() {
private final ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> {
pseudoClassStateChanged(unsaved, !newValue);
};
#Override
protected void updateItem(PowerPlantPM pp, boolean empty) {
PowerPlantPM oldItem = getItem();
if (oldItem != null) {
// remove old listener
oldItem.savedProperty().removeListener(listener);
}
super.updateItem(pp, empty);
if (empty || pp == null) {
// remove pseudoclass from empty cell
pseudoClassStateChanged(unsaved, false);
} else {
// add new listener & handle initial value
pp.savedProperty().addListener(listener);
pseudoClassStateChanged(unsaved, pp.isSaved());
}
}
};
return row;
});
(Of course you need to adjust your CSS selectors to use :unsaved instead of .unsaved.)
I'm building a chat app using JavaFX. Now for chat messages display, I use a simple ListView of ChatItems with a custom cell factory, ChatCell.
ChatItem could be a ChatLabel (server announcements, status change, etc), or a ChatMessage (message sent by self or other parties). And ChatMessage could be extended to show various content (just ChatTextMessage and ChatImageMessage for now)
My custom cell checks the type/characteristics of the item and provides the proper view accordingly.
I'd like to show a timestamp on each ChatMessage view on bottom right corner of the chat bubble. BUT if the text is only one line, I want the timestamp to show on an extra space on the right of the text. (kinda like WhatsApp chat or most chat apps).
Here's a demo:
My current implementation (shown in the demo) uses a boolean property in ChatMessage that determines if the view should be multiline or not. It checks it on every update. I also add a listener to ListView width and call refresh() when it changes. All of this seems excessive. And I don't think adding a "view-related" property to a "model" is a good design. Not to mention that on the first item update, the timestamp is not at the correct place for some reason.
How can I do this properly?
UPDATE 1:
I added a property extractor for the ListView items, and added a listener for the ListView width that calls updateItem on width changes. Now I don't have to call refresh()
Here is ChatCell class:
public class ChatCell extends ListCell<ChatItem> {
private ChatItemView view;
private InvalidationListener listViewListener;
private InvalidationListener listViewWidthListener;
public ChatCell() {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
listViewWidthListener = e -> {
updateItem(getItem(), isEmpty());
};
listViewListener = e -> {
if(getView() == null) return;
getView().unbindWidth();
if(listViewProperty().get() != null) {
getView().bindWidth(listViewProperty().get().widthProperty());
listViewProperty().get().widthProperty().addListener(listViewWidthListener);
listViewWidthListener.invalidated(listViewProperty().get().widthProperty());
}
};
}
public ChatItemView getView() {
return view;
}
private void initChatLabelView() {
view = new ChatLabelView();
}
private void initChatMessageView(ChatMessage message) {
view = new ChatMessageViewWrapper(message);
}
private void resetView() {
if(view == null) return;
view.unbindWidth();
listViewProperty().removeListener(listViewListener);
if(getListView() != null)
getListView().widthProperty().removeListener(listViewWidthListener);
view = null;
}
private void newViewCreated() {
listViewProperty().addListener(listViewListener);
setGraphic(view);
listViewListener.invalidated(listViewProperty());
}
#Override
protected void updateItem(ChatItem item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setGraphic(null);
setText(null);
resetView();
} else {
// First-level comparison: Label vs. Message
if(item.getViewType() == ViewType.LABEL) {
if(view == null || !(view instanceof ChatLabelView)) {
resetView();
initChatLabelView();
newViewCreated();
}
}
else if(item.getViewType() == ViewType.MESSAGE) {
ChatMessage message = (ChatMessage) item;
if(view == null || !(view instanceof ChatMessageViewWrapper)) {
resetView();
initChatMessageView(message);
newViewCreated();
}
else {
ChatMessageViewWrapper bubble = (ChatMessageViewWrapper) view;
// Second-level comparison: Self-message vs. other user message,
// Text vs. Image (or file), multiline vs. oneline
if( (bubble.getContentType() != message.getContentType())
|| (bubble.isSelf() ^ message.isSelf())
|| (bubble.isMultiline() ^ message.isMultiline())) {
resetView();
initChatMessageView(message);
newViewCreated();
}
}
}
else {
return;
}
view.setItem(item);
}
}
#Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
}
}
This question is related to this. Now I want to colour the row where field value equals to some value.
#FXML
private TableView<FaDeal> tv_mm_view;
#FXML
private TableColumn<FaDeal, String> tc_inst;
tc_inst.setCellValueFactory(cellData -> new SimpleStringProperty(""+cellData.getValue().getInstrumentId()));
tc_inst.setCellFactory(column -> new TableCell<FaDeal, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(item);
// Style row where balance < 0 with a different color.
TableRow currentRow = getTableRow();
if (item.equals("1070")) {
currentRow.setStyle("-fx-background-color: tomato;");
} else currentRow.setStyle("");
}
}
});
The problem is I don't want to show tc_inst in my table. For this reason I set visible checkbox in SceneBuilder to false. In this case colouring part doesn't work at all. How can hide tc_inst so that colouring works?
Use a row factory, instead of a cell factory, if you want to change the color of the whole row:
tv_mm_view.setRowFactory(tv -> new TableRow<FaDeal>() {
#Override
public void updateItem(FaDeal item, boolean empty) {
super.updateItem(item, empty) ;
if (item == null) {
setStyle("");
} else if (item.getInstrumentId().equals("1070")) {
setStyle("-fx-background-color: tomato;");
} else {
setStyle("");
}
}
});
Note that if the value of instrumentId changes while the row is displayed, then the color will not change automatically with the above code, unless you do some additional work. The simplest way to make that happen would be to construct your items list with an extractor which returned the instrumentIdProperty() (assuming you are using the JavaFX property pattern in FaDeal).
I have a ComboBox which I'm populating with Sheet object values.
I set a Cell Factory in order to display the sheet's name in the drop down list itself. It works properly (seems so).
The problem is that after an item ("Cell") is selected, the value that is shown in the box is not the value that was shown in the list.
This is the relevant code part:
excelFile = new ExcelFile(file);
//ObservableList<String> sheets = FXCollections.observableArrayList(excelFile.getSheetsNames());
ObservableList<Sheet> sheets = FXCollections.observableArrayList(excelFile.getSheets());
sheetsBox.setItems(sheets);
sheetsBox.setDisable(false);
sheetsBox.setCellFactory(new Callback<ListView<Sheet>, ListCell<Sheet>>() {
#Override
public ListCell<Sheet> call(ListView<Sheet> param) {
return new ListCell<Sheet>() {
#Override
protected void updateItem(Sheet item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.getSheetName());
}
}
};
}
});
This is the problem (visually):
Thank you
The cell used to display the selected item is the buttonCell. So you just need to set the same cell for the button cell. You can factor the cell creation into a method to avoid replicating code:
sheetsBox.setCellFactory(lv -> createSheetCell());
sheetsBox.setButtonCell(createSheetCell());
// ...
private ListCell<Sheet> createSheetCell() {
return new ListCell<Sheet>() {
#Override
protected void updateItem(Sheet item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.getSheetName());
}
}
};
}
I am having a problem that I cannot figure out. I am taking a TreeView called treeModel and setting cells using setCellFactory as can be seen by the code. Now within the updateItem, I am setting a CheckBox as a graphic and am associating it with the CheckBoxTreeItem custom class called CheckBoxTreeItemModel. Now every time updateItem runs a new CheckBox is created and a new ChangeListener is created for it.
Now at first everything looks normal. Then I expand the direct children of the root, and begin checking item, but the listener seems to be called multiple time. For every level of TreeItems that is expanded, that is how many times the listener is called on one of the descendants of root. If I click on a child a few leaves down a parent, those listeners are then called multiple times as well. Its weird behavior that might be hard to explain, but the point is I don't think the listener is suppose to be called that many times. Its as if its cached. The problem code is below. Any help understanding why this may be happening would be greatly appreciated.
treeModel.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
#Override
public TreeCell<String> call(TreeView<String> param) {
return new TreeCell<String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
final TreeCell<String> currCell = this;
this.setOnMouseClicked(new EventHandler<MouseEvent>() {
/*mouse event stuff completely unrelated to problem*/
});
if (empty) {
setText(null);
setGraphic(null);
}
else {
TreeItem<String> treeItem = getTreeItem();
if (treeItem instanceof CheckBoxTreeItemModel) {
System.out.println("Being called.");
final CheckBoxTreeItemModel chkTreeItem = (CheckBoxTreeItemModel) treeItem;
setText(item.toString());
CheckBox chk = new CheckBox();
chk.setSelected(chkTreeItem.getDeleteTick());
if(chkTreeItem.getListener() == null) {
ChangeListener<Boolean> listener = new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if(newValue) {
//was checked
System.out.println(chkTreeItem.toString()+" was checked!");
chkTreeItem.setDeleteTick(newValue);
}
else {
System.out.println(chkTreeItem.toString()+" was un-checked!");
chkTreeItem.setDeleteTick(newValue);
}
}//end of changed method
};
chkTreeItem.setListener(listener);
}
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
setGraphic(chk);
}
else {
setText(item.toString());
setGraphic(null);
}
}
}//end of updateItem
};
}//end of the call method
});
Suggested Approach
I advise scrapping most of your code and using inbuilt CheckBoxTreeCell and CheckBoxTreeItem classes instead. I'm not sure if the inbuilt cells match your requirements, but even if they don't, you could examine the source code for them and compare it with yours and it (should) start to give you a good idea on where you are going wrong.
Potential Issues in Your Code
Reproducing your issue would require more code than you currently supply. But some things to look for are:
Removing and adding the same listener is pointless:
// first line is redundant.
// all listener code is probably unnecessary as you already bindBidirectional.
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
updateItem might be called many times for a given cell. don't create new nodes for the cell everytime, instead re-use existing nodes created for the cell.
// replace with a lazily instantiated CheckBox member reference in TreeCell instance.
CheckBox chk = new CheckBox();
You bindBiDirectional but never unbind those bound values.
// should also unbind this values.
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
Sample Code
Sample updateItem code from the inbuilt CheckBoxTreeCell code:
public class CheckBoxTreeCell<T> extends DefaultTreeCell<T> {
. . .
private final CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
. . .
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter c = getConverter();
TreeItem<T> treeItem = getTreeItem();
// update the node content
setText(c != null ? c.toString(treeItem) : (treeItem == null ? "" : treeItem.toString()));
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
Callback<TreeItem<T>, ObservableValue<Boolean>> callback = getSelectedStateCallback();
if (callback == null) {
throw new NullPointerException(
"The CheckBoxTreeCell selectedStateCallbackProperty can not be null");
}
booleanProperty = callback.call(treeItem);
if (booleanProperty != null) {
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
}
}
}
. . .
}