I am trying to automatically update a JavaFX ListView when a change occurs on a Property located within an ObservableMap.
Below is my model, where I have a Project, containing a list of Seats, and each Seat in turn contains a Map of type <Layer, ObjectProperty<Category>>.
What I am trying to achieve is to bind an ui element to that ObjectProperty<Category> within the Map.
Here is the Model:
public class Seat {
private final DoubleProperty positionX;
private final DoubleProperty positionY;
private final MapProperty<Layer, ObjectProperty<Category>> categoryMap;
public Seat() {
this.positionX = new SimpleDoubleProperty();
this.positionY = new SimpleDoubleProperty();
this.categoryMap = new SimpleMapProperty(FXCollections.observableHashMap());
}
}
public class Project {
private ObservableList<Seat> seatList;
public Project() {
seatList = FXCollections.observableArrayList(
new Callback<Seat, Observable[]>() {
#Override
public Observable[] call(Seat seat) {
return new Observable[]{
seat.categoryMapProperty()
};
}
}
);
}
The UI element I want to bind is a ListView with a custom cell as follows:
public class CategoryCell extends ListCell<Category>{
private ToggleButton viewButton;
private Rectangle colorRect;
private Label name;
private Label count;
private GridPane pane;
public CategoryCell(ObservableList<Seat> seatList) {
super();
buildGui();
itemProperty().addListener((list, oldValue, newValue) -> {
if (newValue != null) {
//Bind color
colorRect.fillProperty().bind(newValue.colorProperty());
//Bind category name
name.textProperty().bind(newValue.nameProperty());
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
if (oldValue != null) {
name.textProperty().unbind();
count.textProperty().unbind();
colorRect.fillProperty().unbind();
}
});
}
private void buildGui() {
FontIcon hidden = new FontIcon("mdi-eye-off");
viewButton = new ToggleButton("");
viewButton.setGraphic(hidden);
viewButton.selectedProperty().addListener((observable,oldValue, newValue) -> {
Category category = itemProperty().get();
if (newValue == true) {
category.shownColorProperty().unbind();
category.setShownColor(Color.TRANSPARENT);
}else {
category.shownColorProperty().bind(category.colorProperty());
}
});
colorRect = new Rectangle(30,30);
name = new Label();
name.setMaxWidth(120);
pane = new GridPane();
count = new Label();
count.setPadding(new Insets(0,0,0,10));
ColumnConstraints nameCol = new ColumnConstraints();
nameCol.setHgrow( Priority.ALWAYS );
pane.getColumnConstraints().addAll(
new ColumnConstraints(40),
new ColumnConstraints(40),
nameCol,
new ColumnConstraints(40));
pane.addColumn(0, viewButton);
pane.addColumn(1, colorRect);
pane.addColumn(2, name );
pane.addColumn(3, count);
this.setText(null);
name.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount()==2) {
//launch the category editor TODO
}
}
});
}
The problem is that the code below is not triggered when I change the Category value of a CategoryProperty within the MapProperty of a Seat.
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
Any advice on how to achieve this?
===== Clarifications following James_D comment ====
1) About the model: I have actually thought and hesitated quite a bit about this. I want to allocate categories to seats in concert halls, and do this on multiple "layers/levels". Say for example a price "layer" where I could have four price tag categories, and "selling company" layer where I would have 3 companies, etc... In order to model this in my Seat class I have a Map<Layer, Category> which looks like a good choice as a seat should only be assigned to one unique category per layer. Then my Project class keeps track of Layers and their respective Categories, which is not really needed but handy to keep their user-specified display order.
2) Thank you for spotting that bug in the CategoryCell! The order of if (oldValue != null) and if (newValue != null) should indeed be reversed.
3) Now what I need to answer my initial question is a way to trigger a notification when the categoryProperty in the Map of the Seat class is modified.
Actually, just refreshing the listview whenever I make a change to my Map solves the issue, but it kinds of defeat the purpose of having a Observable property...
Answering myself now that I understand a little more.
1) How to bind to a property within a ObservableMap?
By using the valueAt() method of a MapProperty.
Instead of using ObservableMap<Layer, ObjectProperty<Category>>, use
MapProperty<Layer, Category>.
2) How to trigger a notification when the objectProperty in the ObservableMap is modified?
Since we are now using a MapProperty where the value is the object and not the property wrapping it, we can just use the addListener() method of the MapProperty.
Related
I'm using a FilteredList for my ListView to enable searching. The problem is that FilteredList does not allow mutating the data in any way, it only responds to changes in underlying ObservableList.
Also, it is declared final, so I can't simply extend it to forward the edit requests to the source.
So, how can I use it in an Editable ListView?
Here is the code to reproduce the problem
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
//problematic code
var observableList = FXCollections.observableArrayList("name", "name 2", "name 3");
FilteredList<String> filteredList = new FilteredList<>(observableList);
var list = new ListView<>(filteredList);
list.setEditable(true);
list.setCellFactory(TextFieldListCell.forListView());
//boilerplate code
VBox wrapper = new VBox(list);
primaryStage.setScene(new Scene(wrapper));
primaryStage.show();
}
}
Edit: added an minimal reproducible example
The problem - as you noticed - is that none of the concrete implementations of TransformationList (Sorted/FilteredList) is modifiable. So the default commit handler fails (with UnsupportedOperationException) while trying to set the newValue:
private EventHandler<ListView.EditEvent<T>> DEFAULT_EDIT_COMMIT_HANDLER = t -> {
int index = t.getIndex();
List<T> list = getItems();
if (index < 0 || index >= list.size()) return;
list.set(index, t.getNewValue());
};
The way out is a custom commit handler. Its implementation depends on context, it can
set a new item in the underlying source list
modify a property of the item
Code snippet for setting an item:
// monolithic items
ObservableList<String> data = FXCollections.observableArrayList("afirst", "abString", "other");
FilteredList<String> filteredData = new FilteredList<>(data);
filteredData.setPredicate(text -> text.contains("a"));
// set up an editable listView
ListView<String> list = new ListView<>(filteredData);
list.setEditable(true);
list.setCellFactory(TextFieldListCell.forListView());
// commitHandler resetting the underlying data element
list.setOnEditCommit(v -> {
ObservableList<String> items = list.getItems();
int index = v.getIndex();
if (items instanceof TransformationList<?, ?>) {
TransformationList transformed = (TransformationList) items;
items = transformed.getSource();
index = transformed.getSourceIndex(index);
}
items.set(index, v.getNewValue());
});
Code snippet for changing a property of an item:
// items with properties
ObservableList<MenuItem> data = FXCollections.observableArrayList(
new MenuItem("afirst"), new MenuItem("abString"), new MenuItem("other"));
FilteredList<MenuItem> filteredData = new FilteredList<>(data);
// filter on text property
filteredData.setPredicate(menuItem -> menuItem.getText().contains("a"));
// set up an editable listView
ListView<MenuItem> list = new ListView<>(filteredData);
list.setEditable(true);
// converter for use in TextFieldListCell
StringConverter<MenuItem> converter = new StringConverter<>() {
#Override
public String toString(MenuItem menuItem) {
return menuItem != null ? menuItem.getText() : null;
}
#Override
public MenuItem fromString(String text) {
return new MenuItem(text);
}
};
list.setCellFactory(TextFieldListCell.forListView(converter));
// commitHandler changing a property of the item
list.setOnEditCommit(v -> {
ObservableList<MenuItem> items = list.getItems();
MenuItem column = items.get(v.getIndex());
MenuItem standIn = v.getNewValue();
column.setText(standIn.getText());
});
I am building an application using JavaFX. What I am trying to do is generate a message according to the user input values. So there are one text-field and one combo-box and one check-box per row and there are many rows like the following.
Let's say I will generate three different messages according to the user values. So I need to check whether those fields are empty or not and check each field's value to generate a specific message. Checking fields are okay for just three rows like the above. But I have 10 fields. So I have to check each and generate or append my own message. And also if the user checked the check-box need to group all checked row values. So what I am asking is there any good way (best practice) to achieve what I need or an easy one also? I have tried with HashMap and ArrayList. But those are not working for this.
Really appreciate it if anybody can help me. Thanks in advance.
I would probably recommend a custom node that you create on your own like below. This example is not supposed to have the same functionality as your application but just to show how to create and use custom nodes. I kept your idea in mind when creating this example it has your textfield combobox and checkbox and they are a little easier to manage. Give it a run and let me know if you have any questions
public class Main extends Application {
#Override
public void start(Stage stage) {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
ArrayList<String> itemList = new ArrayList<>(Arrays.asList("Dog", "Cat", "Turkey"));
ArrayList<HBoxRow> hBoxRowArrayList = new ArrayList<>();
for (int i = 0; i<3; i++) {
HBoxRow hBoxRow = new HBoxRow();
hBoxRow.setComboBoxValues(FXCollections.observableList(itemList));
hBoxRowArrayList.add(hBoxRow);
vBox.getChildren().add(hBoxRow.gethBox());
}
Button printTextfieldsButton = new Button("Print Textfields");
printTextfieldsButton.setOnAction(event -> {
for (HBoxRow hBoxRow : hBoxRowArrayList) {
System.out.println("hBoxRow.getTextFieldInput() = " + hBoxRow.getTextFieldInput());
}
});
vBox.getChildren().add(printTextfieldsButton);
stage.setScene(new Scene(vBox));
stage.show();
}
//Below is the custom Node
public class HBoxRow {
HBox hBox = new HBox();
ComboBox<String> comboBox = new ComboBox<>();
TextField textField = new TextField();
CheckBox checkBox = new CheckBox();
public HBoxRow(){
hBox.setAlignment(Pos.CENTER);
textField.setPrefWidth(150);
comboBox.setPrefWidth(150);
checkBox.setOnAction(event -> {
textField.setDisable(!textField.isDisabled());
comboBox.setDisable(!comboBox.isDisabled());
});
hBox.getChildren().addAll(checkBox, textField, comboBox);
}
public void setComboBoxValues(ObservableList observableList) {
comboBox.setItems(observableList);
}
public HBox gethBox(){
return hBox;
}
public String getTextFieldInput(){
return textField.getText();
}
}
}
I have a tabelview that displays a list of appointees. Each appointe has a group assigned to it, the id of that group is saved in the appointe class.
I want to display a combobox inside a tablecell that displays the selected group and all other groups that exist. I can set the items of the combobox in the cell factory but i cant set the selected value of the respective appointee.
I have a method that returns the Group from the observable list when i provide it with the id. Thats means i need the id in the cellfactory but i didnt find a way to do this. I also need to display the name of the group and not the refernce to the clas. Is there a way to do this, or should i change my approach?
The Appointee class
public class Appointee {
private SimpleIntegerProperty id;
private SimpleStringProperty firstname;
private SimpleStringProperty lastname;
private SimpleIntegerProperty group;
private SimpleIntegerProperty assigned;
public Appointee(int id, String firstname, String lastname, int group, int assigned){
this.id = new SimpleIntegerProperty(id);
this.firstname = new SimpleStringProperty(firstname);
this.lastname = new SimpleStringProperty(lastname);
this.group = new SimpleIntegerProperty(group);
this.assigned = new SimpleIntegerProperty(assigned);
}
The Group class
public class Group {
private IntegerProperty id;
private StringProperty name;
private IntegerProperty members;
private IntegerProperty assigned;
public Group(int id, String name, int members, int assigned) {
this.id = new SimpleIntegerProperty(id);
this.name = new SimpleStringProperty(name);
this.members = new SimpleIntegerProperty(members);
this.assigned = new SimpleIntegerProperty(assigned);
}
The appointe table view
public AppointeeTableView() {
// define table view
this.setPrefHeight(800);
this.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
this.setItems(MainController.appointeeObervableList);
this.setEditable(true);
// define columns
...
TableColumn groupCol = new TableColumn("Group"); // group
groupCol.setCellFactory(col -> {
TableCell<Group, StringProperty> c = new TableCell<>();
final ComboBox<String> comboBox = new ComboBox(MainController.groupObservableList);
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
groupCol.setEditable(false);
...
}
Override the updateItem method of the TableCell to update the cell, make sure the new value is saved on a change of the TableCell value and use a cellValueFactory.
final Map<Integer, Group> groupById = ...
final ObservableList<Integer> groupIds = ...
TableColumn<Group, Number> groupCol = new TableColumn<>("Group");
groupCol.setCellValueFactory(cd -> cd.getValue().groupProperty());
class GroupCell extends ListCell<Integer> {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
Group group = groupById.get(item);
if (empty || group == null) {
setText("");
} else {
setText(group.getName());
}
}
}
groupCol.setCellFactory(col -> new TableCell<Group, Integer>() {
private final ComboBox<Integer> comboBox = new ComboBox<>(groupIds);
private final ChangeListener<Integer> listener = (o, oldValue, newValue) -> {
Group group = (Group) getTableView().getItems().get(getIndex());
group.setGroup(newValue);
};
{
comboBox.setCellFactory(lv -> new GroupCell());
comboBox.setButtonCell(new GroupCell());
}
#Override
protected void updateItem(Number item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
comboBox.valueProperty().removeListener(listener);
setGraphic(comboBox);
comboBox.setValue((Integer) item);
comboBox.valueProperty().addListener(listener);
}
}
});
It's a bit hard to tell from only some small code snippets, but my general recommendation when working with frontends is to distinguish between the model and the rendering on each level. This applies to JavaFX, Swing and Angular applications alike.
The appointee TableView should likely be TableView<Appointee>.
For the appointee.group property you have two options: either use Group or (e.g. when this would generate too many duplicate data when de-/serializing from/ to JSON) then use a business key. The first option is usually easier to implement and work with. With the second option you'll need some service / code to convert back to a Group and have to think about where/ at what level exactly you want to do the conversion.
Let's go on here with the second option as you currently have specified appointee.group to be an integer.
In this case the group column should be TableColum<Appointee, Integer>.
The group cell then should be TableCell<Appointee, Integer>.
So far we've only talked about the model, not about rendering except that we want to display the appointees in a table.
I recommend to do this also on the next level.
Don't use a ComboBox<String> for a groups comboBox but a ComboBox<Group>. String is how you want to render the group inside the comboBox but the Group is the model. Also ComboBox<Integer>, the type of the business key, is a bit misleading (as you want a Groups comboBox, not an integer comboBox) and limits the flexibility of your code.
Use the converting service / code I've mentioned when pre-selecting a value in the comboBox.
The group cell should have the type ListCell<Group> and in the updateItem method, which concerns about how to render a Group, you could e.g. use the name property to get the String representation.
Of course there are variations of this approach, but make sure that on each level you know what the model of the control is and what the renderer of the control is. Always design your code using the model and use the rendering types only at the lowest rendering level.
I have a restaurant menu with dishes and categories implemented as a treeTableView in javaFX.
I want to make the the category rows appear different with CSS but I just can't find a way to filter them out and apply a class. Moving the images a bit to the left would also be nice. I also had no luck using a rowFactory. I've seen this answer but I don't understand it.
This is how I fill the table. I've left out the column- and cellfactories.
private void fillDishes(List<Dish> dishes){
root.getChildren().clear();
Map<String,TreeItem<Dish>> categoryMap = new HashMap<>();
for (Category c: allCats) {
TreeItem<Dish> newCat = new TreeItem<>(new Dish(c.getName(),null,null,null));
//newCat.getGraphic().getStyleClass().add("category");
categoryMap.put(c.getName(),newCat);
root.getChildren().add(newCat);
}
for (Dish d: dishes) {
categoryMap.get(d.getCategory()).getChildren().add(new TreeItem<>(d));
}
}
TreeTableView uses the rowFactory to create the TreeTableRows. At some time later it assigns a TreeItem to a TreeTableRow. This may happen again with different TreeItems for the same row. For this reason you need to handle changes those changes which can be done by adding a ChangeHandler to the TreeTableRow.treeItem property. If a new TreeItem is assigned to the row, you can check for top-level nodes by checking the children of the (invisible) root item for the row item.
I prefer the approach that does not require searching the child list though. It's possible to compare the parent of the item with the root.
public static class Item {
private final String value1;
private final String value2;
public Item(String value1, String value2) {
this.value1 = value1;
this.value2 = value2;
}
public String getValue1() {
return value1;
}
public String getValue2() {
return value2;
}
}
#Override
public void start(Stage primaryStage) {
final TreeItem<Item> root = new TreeItem<>(null);
TreeTableView<Item> ttv = new TreeTableView<>(root);
ttv.setShowRoot(false);
TreeTableColumn<Item, String> column1 = new TreeTableColumn<>();
column1.setCellValueFactory(new TreeItemPropertyValueFactory<>("value1"));
TreeTableColumn<Item, String> column2 = new TreeTableColumn<>();
column2.setCellValueFactory(new TreeItemPropertyValueFactory<>("value2"));
ttv.getColumns().addAll(column1, column2);
final PseudoClass topNode = PseudoClass.getPseudoClass("top-node");
ttv.setRowFactory(t -> {
final TreeTableRow<Item> row = new TreeTableRow<>();
// every time the TreeItem changes, check, if the new item is a
// child of the root and set the pseudoclass accordingly
row.treeItemProperty().addListener((o, oldValue, newValue) -> {
boolean tn = false;
if (newValue != null) {
tn = newValue.getParent() == root;
}
row.pseudoClassStateChanged(topNode, tn);
});
return row;
});
// fill tree structure
TreeItem<Item> c1 = new TreeItem<>(new Item("category 1", null));
c1.getChildren().addAll(
new TreeItem<>(new Item("sub1.1", "foo")),
new TreeItem<>(new Item("sub1.2", "bar")));
TreeItem<Item> c2 = new TreeItem<>(new Item("category 2", null));
c2.getChildren().addAll(
new TreeItem<>(new Item("sub2.1", "answer")),
new TreeItem<>(new Item("sub2.2", "42")));
root.getChildren().addAll(c1, c2);
Scene scene = new Scene(ttv);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
style.css
.tree-table-row-cell:top-node {
-fx-background: orange;
}
Moving the images a bit to the left would also be nice.
Usually you do this from a custom TreeTableCell returned by a TreeTableColumn.cellFactory. Depending on the behavior you want to implement setting fitWidth/fitHeight may be sufficient, but in other cases dynamically modifying those values based on the cell size may be required.
I would like to add a listener when I am clicking the cell for categories only.
this is the declaration of my columnConfig
ColumnConfig<UserRights, Boolean> unlockConfig = new ColumnConfig<UserRights, Boolean>(properties.hasUnlock(), 50);
unlockConfig.setHeader("Unlock");
cfgs.add(unlockConfig);
ColumnConfig<UserRights, String> catConfig = new ColumnConfig<UserRights, String>(properties.categories(), 150);
catConfig.setHeader("Categories");
cfgs.add(catConfig);
cm = new ColumnModel<UserRights>(cfgs);
grid = new Grid<UserRights>(store, cm);
grid.getView().setAutoFill(true);
grid.addStyleName("margin-10");
grid.setLayoutData(new VerticalLayoutContainer.VerticalLayoutData(1, 1));
grid.addRowClickHandler(new RowClickEvent.RowClickHandler() {
#Override
public void onRowClick(RowClickEvent event) {
index = event.getRowIndex();
}
});
rowEditing = new GridRowEditing<UserRights>(grid);
rowEditing.addEditor(unlockConfig, new CheckBox());
how could I add a listener in the category column?
Thanks in advance.
There is nothing in your categories cell that prompts a user to click, it just contains text. What you should be doing is using the columnConfig.setCell(Cell cell) method to specify a cell that contains an interactive component.
You could still try something along these lines:
columnConfig.setCell(new SimpleSafeHtmlCell<String>(SimpleSafeHtmlRenderer.getInstance(), "click") {
#Override
public void onBrowserEvent(Context context, Element parent, String value, NativeEvent event, ValueUpdater<String> valueUpdater) {
super.onBrowserEvent(context, parent, value, event, valueUpdater);
if (event.getType().equals("click")) {
}
}
});