Add expand animation to TreeView - javafx

I have this example of TreeView:
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
public class MainApp extends Application
{
//private final Node rootIcon = new ImageView(new Image(getClass().getResourceAsStream("picture.png")));
//private final Image depIcon = new Image(getClass().getResourceAsStream("picture.png"));
List<Employee> employees = Arrays.<Employee>asList(
new Employee("Ethan Williams", "Sales Department"),
new Employee("Emma Jones", "Sales Department"),
new Employee("Michael Brown", "Sales Department"),
new Employee("Anna Black", "Sales Department"),
new Employee("Rodger York", "Sales Department"),
new Employee("Susan Collins", "Sales Department"),
new Employee("Mike Graham", "IT Support"),
new Employee("Judy Mayer", "IT Support"),
new Employee("Gregory Smith", "IT Support"),
new Employee("Jacob Smith", "Accounts Department"),
new Employee("Isabella Johnson", "Accounts Department"));
TreeItem<String> rootNode = new TreeItem<>("MyCompany Human Resources");//, rootIcon); // Set picture
public static void main(String[] args)
{
Application.launch(args);
}
#Override
public void start(Stage stage)
{
rootNode.setExpanded(true);
for (Employee employee : employees)
{
TreeItem<String> empLeaf = new TreeItem<>(employee.getName());
boolean found = false;
for (TreeItem<String> depNode : rootNode.getChildren())
{
if (depNode.getValue().contentEquals(employee.getDepartment()))
{
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found)
{
TreeItem<String> depNode = new TreeItem<>(
employee.getDepartment()//,new ImageView(depIcon) // Set picture
);
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
TreeView<String> treeView = new TreeView<>(rootNode);
//treeView.setEditable(true);
treeView.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>()
{
#Override
public TreeCell<String> call(TreeView<String> p)
{
return new TextFieldTreeCellImpl();
}
});
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
private final class TextFieldTreeCellImpl extends TreeCell<String>
{
private TextField textField;
public TextFieldTreeCellImpl()
{
}
#Override
public void startEdit()
{
super.startEdit();
if (textField == null)
{
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit()
{
super.cancelEdit();
setText((String) getItem());
setGraphic(getTreeItem().getGraphic());
}
#Override
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
if (empty)
{
setText(null);
setGraphic(null);
}
else
{
if (isEditing())
{
if (textField != null)
{
textField.setText(getString());
}
setText(null);
setGraphic(textField);
}
else
{
setText(getString());
setGraphic(getTreeItem().getGraphic());
}
}
}
private void createTextField()
{
textField = new TextField(getString());
textField.setOnKeyReleased(new EventHandler<KeyEvent>()
{
#Override
public void handle(KeyEvent t)
{
if (t.getCode() == KeyCode.ENTER)
{
commitEdit(textField.getText());
}
else if (t.getCode() == KeyCode.ESCAPE)
{
cancelEdit();
}
}
});
}
private String getString()
{
return getItem() == null ? "" : getItem().toString();
}
}
public static class Employee
{
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private Employee(String name, String department)
{
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
}
public String getName()
{
return name.get();
}
public void setName(String fName)
{
name.set(fName);
}
public String getDepartment()
{
return department.get();
}
public void setDepartment(String fName)
{
department.set(fName);
}
}
}
I want to add animation when I expand the tree. How this can be done?

Take a look at this I found searching online. I found it here, after googling "javafx TreeView animation".
Ok, so after spending a few moments examining the code, I have deduced that the important part of the code is this:
rootItem.expandedProperty().addListener(new ChangeListener<Boolean>() {
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
new Timeline(
new KeyFrame(Duration.seconds(0), new KeyValue(tree.opacityProperty(), 0)),
new KeyFrame(Duration.seconds(1), new KeyValue(tree.opacityProperty(), 1.0))
).play();
}
});
This code adds a listener that triggers code that performs the desired animation every time rootItem.expandedProperty() changes, i.e. the user expanded or collapsed the tree.
Every time that something causes expandedProperty to change, it constructs a new Timeline object, which I assume represents the actual steps in a particular animation. The current code is modifying the opacity of the tree, causing rootItem's subtree to 'fade in' over the course of 1 second.
To implement a seperate animation for closing the tree, you can use the method parameters of changed in the listener to differentiate between the two cases.
In order for such an animation to occur with each subtree, such a listener will need to be added to every node in the tree that has children. Enumerating all the elements of a tree is an extremely basic operation you learned when you learned data structures, so I don't need to go into exactly how to accomplish this.
However, if the fade-in animation is not good enough for your application, then I would suggest using a nested accordion, like this question does. From just the sample items you give, an accordion would be a better way to show the employee lists anyway.

Related

How to create a ListView of complex objects and allow editing a field on the object?

I want to have a JavaFX ListView of Person objects. I want the list to display only the name and allow the name to be edited. It should also preserve the other fields in each object after committing an edit to the name. How would you do this idiomatically in JavaFX?
I have the following code, which works, but it's kind of wonky because it has a StringConverter that converts one way from Person to a String of the person's name then doesn't do the reverse conversion and instead relies on the list cell commitEdit method to take a string of the name and set it on the appropriate person.
Here's the code:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("My Custom List View");
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
ListView<Person> listView = new ListView();
listView.setCellFactory(new CustomCellFactory());
listView.setEditable(true);
listView.setItems(people);
Scene scene = new Scene(listView,400,300);
stage.setScene(scene);
stage.show();
}
public static class CustomCellFactory implements Callback<ListView<Person>,ListCell<Person>> {
#Override
public ListCell<Person> call(ListView param) {
TextFieldListCell<Person> cell = new TextFieldListCell() {
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
System.out.println("updating item: "+item.toString());
setText(((Person) item).getName());
} else {
setText(null);
}
}
#Override
public void commitEdit(Object newName) {
((Person)getItem()).setName((String)newName);
super.commitEdit(getItem());
}
};
cell.setConverter(new StringConverter() {
#Override
public String toString(Object person) {
return ((Person)person).getName();
}
#Override
public Object fromString(String string) {
return string;
}
});
return cell;
}
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
}
TextFieldListCell is just a convenience implementation of ListCell that provides the most common form of editing for list cells (i.e. if the items in the list are Strings, or objects that have an easy conversion to and from strings). You'll often find that you need more specific editing (e.g. you'll often want to filter the text allowed in the editing text field using a TextFormatter), and in that case you just implement the ListCell yourself. I think this is a case where, on balance, it makes more sense to implement ListCell from scratch.
It seems you can force the TextFieldListCell to work for this use case, using:
listView.setCellFactory(lv -> {
TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
cell.setConverter(new StringConverter<Person>() {
#Override
public String toString(Person person) {
return person.getName();
}
#Override
public Person fromString(String string) {
Person person = cell.getItem();
person.setName(string);
return person ;
}
});
return cell;
});
(Note that in your code, your updateItem() method is equivalent to the one already implemented in TextFieldListCell, so it's redundant, and the extra functionality in commitEdit(...) is now in the (typesafe) StringConverter, so there's no longer any need for a subclass.)
This just feels a little fragile, as it relies on a particular implementation of committing the new value from the text field and its interaction with the string converter, but it seems to work fine in tests.
My preference for this, however, would be to implement the ListCell directly yourself, as it gives you full control over the interaction between the text field and the editing process. This is pretty straightforward:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
ListView<Person> listView = new ListView<>();
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
listView.setEditable(true);
listView.setItems(people);
listView.setCellFactory(lv -> new ListCell<Person>() {
private TextField textField = new TextField() ;
{
textField.setOnAction(e -> {
commitEdit(getItem());
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(person.getName());
setText(null);
setGraphic(textField);
} else {
setText(person.getName());
setGraphic(null);
}
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(getItem().getName());
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem().getName());
setGraphic(null);
}
#Override
public void commitEdit(Person person) {
super.commitEdit(person);
person.setName(textField.getText());
setText(textField.getText());
setGraphic(null);
}
});
// for debugging:
listView.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
listView.getItems().forEach(p -> System.out.println(p.getName()));
}
});
Scene scene = new Scene(listView,400,300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
public static void main(String[] args) {
launch(args);
}
}
If you might need this kind of functionality frequently, you could easily create a reusable class:
import java.util.function.BiFunction;
import java.util.function.Function;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class EditingListCell<T> extends ListCell<T> {
private final TextField textField ;
private final Function<T, String> propertyAccessor ;
public EditingListCell(Function<T, String> propertyAccessor, BiFunction<String, T, T> updater) {
this.propertyAccessor = propertyAccessor ;
this.textField = new TextField();
textField.setOnAction(e -> {
T newItem = updater.apply(textField.getText(), getItem());
commitEdit(newItem);
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(propertyAccessor.apply(item));
setText(null);
setGraphic(textField);
} else {
setText(propertyAccessor.apply(item));
setGraphic(null);
}
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(propertyAccessor.apply(getItem()));
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
#Override
public void commitEdit(T item) {
super.commitEdit(item);
getListView().getItems().set(getIndex(), item);
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
}
and then you just need
listView.setCellFactory(lv -> new EditingListCell<>(
Person::getName,
(text, person) -> {
person.setName(text);
return person ;
})
);

JavaFX editable cell with focus change to different populated cell

I need editable cells for JavaFX TableView. The default TextFieldTableCell requires the user to press enter to commit a change. I think a typical user expects the change to be kept when clicking outside the cell. All the features I want include:
Single-click selects the cell and
Another single-click on a cell, in the selected cell, or enter press, starts editing.
A double-click on a cell starts editing.
Pressing enter commits the changes to the cell
Changing mouse focus anywhere outside the cell commits the changes to the cell
I found a EditCell version in this post
It fulfills the first 4 requirements and partially the 5th, but when the user clicks on another populated cell in the table the edit changes are lost. The focus listener is triggered, but no commit. Clicking on an empty cell or another scene element commits the changes.
There is a supposedly a solution provided in post
However, the solution only contains snippets of code instead of a working example. I was not able to implement it.
Can anyone help put the pieces together and demo a class than extends TableCell that has all the features I listed above?
I'm probably a little late to this party but here goes.
Not being able to commit the value of the changed cell is probably due to the default implementation of the commitEdit method in TableCell, as it treats a loss of focus as a cancel-action by default.
However, user James_D created a nice workaround here
EDIT:
Example class based on James_D's work
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class TableViewCommitOnFocusLoss extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
Button showDataButton = new Button("Debug data");
showDataButton.setOnAction(event -> table.getItems().stream()
.map(p -> String.format("%s %s", p.getFirstName(), p.getLastName()))
.forEach(System.out::println));
Scene scene = new Scene(new BorderPane(table, null, null, showDataButton, null), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private <T> TableColumn<T, String> createColumn(String title, Function<T, StringProperty> property) {
TableColumn<T, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> EditCell.createStringEditCell());
return col ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
class EditCell<S, T> extends TableCell<S, T> {
// Text field for editing
// TODO: allow this to be a plugable control.
private final TextField textField = new TextField();
// Converter for converting the text in the text field to the user type, and vice-versa:
private final StringConverter<T> converter ;
public EditCell(StringConverter<T> converter) {
this.converter = converter ;
itemProperty().addListener((obx, oldItem, newItem) -> {
if (newItem == null) {
setText(null);
} else {
setText(converter.toString(newItem));
}
});
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt -> {
commitEdit(this.converter.fromString(textField.getText()));
});
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (! isNowFocused) {
commitEdit(this.converter.fromString(textField.getText()));
}
});
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
textField.setText(converter.toString(getItem()));
cancelEdit();
event.consume();
} else if (event.getCode() == KeyCode.RIGHT) {
getTableView().getSelectionModel().selectRightCell();
event.consume();
} else if (event.getCode() == KeyCode.LEFT) {
getTableView().getSelectionModel().selectLeftCell();
event.consume();
} else if (event.getCode() == KeyCode.UP) {
getTableView().getSelectionModel().selectAboveCell();
event.consume();
} else if (event.getCode() == KeyCode.DOWN) {
getTableView().getSelectionModel().selectBelowCell();
event.consume();
}
});
}
/**
* Convenience converter that does nothing (converts Strings to themselves and vice-versa...).
*/
public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
/**
* Convenience method for creating an EditCell for a String value.
* #return
*/
public static <S> EditCell<S, String> createStringEditCell() {
return new EditCell<S, String>(IDENTITY_CONVERTER);
}
// set the text of the text field and display the graphic
#Override
public void startEdit() {
super.startEdit();
textField.setText(converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
// revert to text display
#Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
// commits the edit. Update property if possible and revert to text display
#Override
public void commitEdit(T item) {
// This block is necessary to support commit on losing focus, because the baked-in mechanism
// sets our editing state to false before we can intercept the loss of focus.
// The default commitEdit(...) method simply bails if we are not editing...
if (! isEditing() && ! item.equals(getItem())) {
TableView<S> table = getTableView();
if (table != null) {
TableColumn<S, T> column = getTableColumn();
TableColumn.CellEditEvent<S, T> event = new TableColumn.CellEditEvent<>(table,
new TablePosition<S,T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}

Custom ListCell implements InvalidationListener, repaint components

I have a custom ListCell implemented that contains a BorderPane layout with some components.
The cell registers itself to the item. So when the duration of the item changes the invalidated method is called.
In this method I set the text of the duration label. My problem is now the method is called but the label is not repainted.
I think if setText is called the cell should repaint. It is possible to manually repaint the cell or the Label.?
public static class ListItemCell extends ListCell<MusicListItem> implements InvalidationListener{
private AnchorPane listItem;
private Label artist;
private Label title;
private Label duration;
private BorderPane borderPane;
private FlowPane flowPane;
public ListItemCell() {
initCellLayout();
}
public ListItemCell(final LogicInterfaceFX logic) {
...
}
public void initCellLayout() {
try {
this.listItem = (AnchorPane) FXMLLoader.load(getClass().getResource("/de/roth/jsona/view/themes/" + Config.getInstance().THEME + "/" + "layout_list_item.fxml"));
} catch (Exception e) {
e.printStackTrace();
}
this.borderPane = (BorderPane) listItem.getChildren().get(0);
this.flowPane = (FlowPane) borderPane.getLeft();
this.artist = (Label) flowPane.getChildren().get(0);
this.artist.getStyleClass().add(defaultTextClass);
this.title = (Label) flowPane.getChildren().get(1);
this.title.getStyleClass().add(defaultTextClass);
this.duration = (Label) borderPane.getRight();
this.duration.getStyleClass().add(defaultTextClass);
this.setGraphic(listItem);
}
#Override
public void updateItem(MusicListItem item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
item.addListener(this);
item.durationProperty().addListener(this);
// Duration
this.duration.setText(item.getDuration());
// Artist / Title
if (item.getArtist() != null) {
this.artist.setText(item.getArtist());
this.title.setText(" - " + item.getTitle());
} else {
this.artist.setText("");
this.title.setText(item.getFile().getName());
}
} else {
this.artist.setText("");
this.title.setText("");
this.duration.setText("");
}
}
#Override
public void invalidated(Observable observable) {
System.out.println("INVALIDATE!!!" + getItem().getFile().getAbsolutePath());
this.duration.setText(getItem().getDuration());
}
}
You have a bug in there: you need to make sure you remove listeners from old items when the item is updated. Remember that ListCells are reused, so updateItem(...) is called multiple times during the lifespan of your ListView.
I don't know if that's what is causing it to fail to update. This works for me:
import java.util.Random;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewUpdatableProperties extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<Item> listView = new ListView<>();
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
final Random rng = new Random();
for (int i=1; i<=20; i++) {
listView.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
BorderPane root = new BorderPane();
root.setCenter(listView);
listView.setCellFactory(new Callback<ListView<Item>, ListCell<Item>>() {
#Override
public ListCell<Item> call(ListView<Item> param) {
return new ItemListCell();
}
});
HBox controls = new HBox();
controls.setPadding(new Insets(5));
Button incButton = new Button("Increment selected");
incButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (Item item : listView.getSelectionModel().getSelectedItems()) {
item.increment();
}
}
});
controls.getChildren().add(incButton);
root.setBottom(controls);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class ItemListCell extends ListCell<Item> implements InvalidationListener {
private final HBox hbox ;
private final Label nameLabel ;
private final Label valueLabel ;
public ItemListCell() {
hbox = new HBox(5);
nameLabel = new Label();
valueLabel = new Label();
hbox.getChildren().addAll(nameLabel, valueLabel);
setGraphic(hbox);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(Item item, boolean empty) {
Item oldItem = getItem();
if (oldItem != null) {
oldItem.valueProperty().removeListener(this);
}
super.updateItem(item, empty);
if (item != null) {
nameLabel.setText(item.getName());
valueLabel.setText(String.valueOf(item.getValue()));
item.valueProperty().addListener(this);
} else {
nameLabel.setText("");
valueLabel.setText("");
}
}
#Override
public void invalidated(Observable observable) {
final int value = getItem().getValue();
System.out.println("Invalidated: item is "+getItem().getName() + " with value "+value);
valueLabel.setText(String.valueOf(value));
}
}
public static class Item {
public Item(String name, int value) {
setName(name);
setValue(value);
}
private final StringProperty name = new SimpleStringProperty(this, "name");
public StringProperty nameProperty() {
return name ;
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
}
private final IntegerProperty value = new SimpleIntegerProperty(this, "value");
public IntegerProperty valueProperty() {
return value ;
}
public int getValue() {
return value.get();
}
public void setValue(int value) {
this.value.set(value);
}
public void increment() {
value.set(value.get()+1);
}
}
public static void main(String[] args) {
launch(args);
}
}
As stated in the other answer, there is no repaint() method in JavaFX. If you wire things up correctly, when the properties are invalidated, it will know to repaint.
JavaFX uses a "retained mode" rendering model whereas Swing uses an "immediate mode".
See: Retained Mode Versus Immediate Mode (Windows)
So, you don't have a direct repaint() method.

Context menu for JavaFX tree

I want to create context menu for JavaFX. This is the code that I tested. But for some reason there is no context menu when I right-click on the tree node. Can you help me to find my mistake.
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ContextMenuBuilder;
import javafx.scene.control.MenuItemBuilder;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
public class MainApp extends Application
{
List<Employee> employees = Arrays.<Employee>asList(
new Employee("New Chassi", "New Datacenter"),
new Employee("New Battery", "New Rack"),
new Employee("New Chassi", "New Server"),
new Employee("Anna Black", "Sales Department"),
new Employee("Rodger York", "Sales Department"),
new Employee("Susan Collins", "Sales Department"),
new Employee("Mike Graham", "IT Support"),
new Employee("Judy Mayer", "IT Support"),
new Employee("Gregory Smith", "IT Support"),
new Employee("Jacob Smith", "Accounts Department"),
new Employee("Isabella Johnson", "Accounts Department"));
TreeItem<String> rootNode = new TreeItem<>("MyCompany Human Resources");
public static void main(String[] args)
{
Application.launch(args);
}
TreeView<String> treeView = new TreeView<>(rootNode);
#Override
public void start(Stage stage)
{
rootNode.setExpanded(true);
for (Employee employee : employees)
{
TreeItem<String> empLeaf = new TreeItem<>(employee.getName());
boolean found = false;
for (TreeItem<String> depNode : rootNode.getChildren())
{
if (depNode.getValue().contentEquals(employee.getDepartment()))
{
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found)
{
TreeItem<String> depNode = new TreeItem<>(
employee.getDepartment()//,new ImageView(depIcon) // Set picture
);
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
treeView.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>()
{
#Override
public TreeCell<String> call(TreeView<String> arg0)
{
// custom tree cell that defines a context menu for the root tree item
return new MyTreeCell();
}
});
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
public static class Employee
{
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private Employee(String name, String department)
{
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
}
public String getName()
{
return name.get();
}
public void setName(String fName)
{
name.set(fName);
}
public String getDepartment()
{
return department.get();
}
public void setDepartment(String fName)
{
department.set(fName);
}
}
class MyTreeCell extends TextFieldTreeCell<String>
{
private ContextMenu rootContextMenu;
public MyTreeCell()
{
// instantiate the root context menu
rootContextMenu
= ContextMenuBuilder.create()
.items(
MenuItemBuilder.create()
.text("Menu Item")
.onAction(
new EventHandler<ActionEvent>()
{
#Override
public void handle(ActionEvent arg0)
{
System.out.println("Menu Item Clicked!");
}
}
)
.build()
)
.build();
}
#Override
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
// if the item is not empty and is a root...
if (!empty && getTreeItem().getParent() == null)
{
setContextMenu(rootContextMenu);
}
}
}
}
You should assign the context Menu to the TreeView instead of assigning it to the cell factory. Here is the code with the context menu working:
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ContextMenuBuilder;
import javafx.scene.control.MenuItemBuilder;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
public class Test extends Application
{
List<Employee> employees = Arrays.<Employee>asList(
new Employee("New Chassi", "New Datacenter"),
new Employee("New Battery", "New Rack"),
new Employee("New Chassi", "New Server"),
new Employee("Anna Black", "Sales Department"),
new Employee("Rodger York", "Sales Department"),
new Employee("Susan Collins", "Sales Department"),
new Employee("Mike Graham", "IT Support"),
new Employee("Judy Mayer", "IT Support"),
new Employee("Gregory Smith", "IT Support"),
new Employee("Jacob Smith", "Accounts Department"),
new Employee("Isabella Johnson", "Accounts Department"));
TreeItem<String> rootNode = new TreeItem<>("MyCompany Human Resources");
public static void main(String[] args)
{
Application.launch(args);
}
TreeView<String> treeView = new TreeView<>(rootNode);
#Override
public void start(Stage stage)
{
// instantiate the root context menu
ContextMenu rootContextMenu
= ContextMenuBuilder.create()
.items(
MenuItemBuilder.create()
.text("Menu Item")
.onAction(
new EventHandler<ActionEvent>()
{
#Override
public void handle(ActionEvent arg0)
{
System.out.println("Menu Item Clicked!");
}
}
)
.build()
)
.build();
treeView.setContextMenu(rootContextMenu);
rootNode.setExpanded(true);
for (Employee employee : employees)
{
TreeItem<String> empLeaf = new TreeItem<>(employee.getName());
boolean found = false;
for (TreeItem<String> depNode : rootNode.getChildren())
{
if (depNode.getValue().contentEquals(employee.getDepartment()))
{
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found)
{
TreeItem<String> depNode = new TreeItem<>(
employee.getDepartment()//,new ImageView(depIcon) // Set picture
);
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
public static class Employee
{
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private Employee(String name, String department)
{
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
}
public String getName()
{
return name.get();
}
public void setName(String fName)
{
name.set(fName);
}
public String getDepartment()
{
return department.get();
}
public void setDepartment(String fName)
{
department.set(fName);
}
}
}
ContextMenuBuilder and MenuItemBuilder (which are used in the accepted answer) are deprecated in JavaFX 8 and will be removed in future releases.
Following an JavaFX 8 example with two items, one with icon and one without an icon:
MenuItem entry1 = new MenuItem("Test with Icon", graphicNode);
entry1.setOnAction(ae -> ...);
MenuItem entry2 = new MenuItem("Test without Icon");
entry2.setOnAction(ae -> ...);
myTreeView.setContextMenu(new ContextMenu(entry1, entry2));
I know this was 3 years ago but I hope my solution that I just found can help others like me when I was trying to find a simpler solution to this. What I found is you can just create a Label and set that as the graphic for the TreeItem. Then just set the context menu for that label, here is the code to make it work.
//The "" is important, without it the tree item would not appear at all
TreeItem<String> childItem = new TreeItem<>("");
//Create a label with the string you would use for the tree item
childItem.setGraphic(new Label("TreeItem Label"));
//Add menu items for contextMenu
ContextMenu contextMenu = new ContextMenu();
childItem.getGraphic().setOnContextMenuRequested(e -> {
//Sets the context menu for the label.
contextMenu.show(childItem.getGraphic(), e.getScreenX(), e.getScreenY()));
}
Minimal working example below without the unnecessary code and deprecated functions. The solution to .setContextMenu to the entire tree is not really usable since different TreeItems might have different Menus. The code below uses the standard way of using the implemented factory pattern in JavaFX and allows to use of all functions from the JavaFX API:
package javaapplication2;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.stage.Stage;
import javafx.util.Callback;
public class JavaApplication2 extends Application {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
//Start a new TreeView
TreeView<String> tv = new TreeView<>(new TreeItem<>("Root"));
//Set the cell factory
tv.setCellFactory(new Callback() {
#Override
public Object call(Object p) {
return new TreeCellWithMenu();
}
});
//Fill tree with some things
tv.getRoot().getChildren().add(new TreeItem<>("1"));
tv.getRoot().getChildren().add(new TreeItem<>("2"));
tv.getRoot().getChildren().add(new TreeItem<>("3"));
//Stage a new scene
stage.setScene(new Scene(tv));
//Show the stage
stage.show();
}
public class TreeCellWithMenu extends TextFieldTreeCell<String> {
ContextMenu men;
public TreeCellWithMenu() {
//ContextMenu with one entry
men = new ContextMenu(new MenuItem("Right Click"));
}
#Override
public void updateItem(String t, boolean bln) {
//Call the super class so everything works as before
super.updateItem(t, bln);
//Check to show the context menu for this TreeItem
if (showMenu(t, bln)) {
setContextMenu(men);
}else{
//If no menu for this TreeItem is used, deactivate the menu
setContextMenu(null);
}
}
//Deccide if a menu should be shown or not
private boolean showMenu(String t, boolean bln){
if (t != null && !t.equals("Root")) {
return true;
}
return false;
}
}
}

Refer to data fields in TreeView Type other than String

I have used the sample Tree User interface on the JAVA FX 2 site and changed the type from String to PayString and all seems to work well. My next step is in the setCellFactory 'TextFieldTreeCellImpl' I need to assign a different context menu to the cells depending upon the value of PayString.level an integer.
How do I reference the value of the data fields associated with the current cell.
Below are the two source files code in packages treeviewsample. The position I need the data is marked with +++++++++++++++++++++++++ for ease of finding.
`/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package treeviewsample;
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.VBox;
public class TreeViewSample extends Application {
private final Node rootIcon;
private final Image depIcon;
List<Employee> employees = Arrays.<Employee>asList(
new Employee(1, "Ethan Williams", "Sales Department"),
new Employee(2, "Emma Jones", "Sales Department"),
new Employee(3, "Michael Brown", "Sales Department"),
new Employee(4, "Anna Black", "Sales Department"),
new Employee(5, "Rodger York", "Sales Department"),
new Employee(6, "Susan Collins", "Sales Department"),
new Employee(7, "Mike Graham", "IT Support"),
new Employee(8, "Judy Mayer", "IT Support"),
new Employee(9, "Gregory Smith", "IT Support"),
new Employee(10, "Jacob Smith", "Accounts Department"),
new Employee(11, "Isabella Johnson", "Accounts Department"));
TreeItem<PayString> rootNode;
Integer next;
public static void main(String[] args) {
Application.launch(args);
}
public TreeViewSample() {
this.next = 12;
this.depIcon = new Image(getClass().getResourceAsStream("department.png"));
this.rootIcon = new ImageView(new Image(getClass().getResourceAsStream("root.png")));
this.rootNode = new TreeItem<>(new PayString ("MyCompany Human Resources", 0,0), rootIcon);
}
#Override
public void start(Stage stage) {
rootNode.setExpanded(true);
for (Employee employee : employees) {
TreeItem<PayString> empLeaf = new TreeItem<>(new PayString(employee.getName(),2,employee.getId()));
boolean found = false;
for (TreeItem<PayString> depNode : rootNode.getChildren()) {
if (depNode.getValue().toString().contentEquals(employee.getDepartment())){
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found) {
TreeItem depNode = new TreeItem<>(new PayString(employee.getDepartment(),1,employee.getId()),
new ImageView(depIcon)
);
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
TreeView<PayString> treeView = new TreeView<>(rootNode);
treeView.setEditable(true);
treeView.setCellFactory(new Callback<TreeView<PayString>,TreeCell<PayString>>(){
#Override
public TreeCell<PayString> call(TreeView<PayString> p) {
return new TextFieldTreeCellImpl();
}
});
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
private final class TextFieldTreeCellImpl extends TreeCell<PayString> {
private TextField textField;
private ContextMenu addMenu = new ContextMenu();
/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* This is where I need to be able to extract the values in the current TreeCell<PayString>
* to be able to create the appropriate context menu.
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
private String curr = getString();
public TextFieldTreeCellImpl() {
TreeItem<PayString> paystring = treeItemProperty().getValue();
MenuItem addMenuItem = new MenuItem("Add Employee");
MenuItem addMenuItem2 = new MenuItem("Add Address");
MenuItem addMenuItem3 = new MenuItem(curr);
addMenu.getItems().add(addMenuItem2);
addMenu.getItems().add(addMenuItem3);
addMenuItem.setOnAction(new EventHandler() {
#Override
public void handle(Event t) {
TreeItem newEmployee =
new TreeItem<>(new PayString ("New Employee",3,next));
next ++;
getTreeItem().getChildren().add(newEmployee);
}
});
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getString());
setGraphic(getTreeItem().getGraphic());
}
#Override
public void updateItem(PayString item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
if (
!getTreeItem().isLeaf()&&getTreeItem().getParent()!= null
){
setContextMenu(addMenu);
}
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(new PayString (textField.getText(),3,next));
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
public static class Employee {
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private final Integer id;
private Employee(Integer id, String name, String department) {
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
this.id = new Integer(id);
}
public Integer getId() {
return id;
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public String getDepartment() {
return department.get();
}
public void setDepartment(String fName) {
department.set(fName);
}
}
}
package treeviewsample;
import java.util.Objects;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* #author Len
*/
public class PayString {
private final String description;
private final Integer level;
private final Integer id;
public PayString(String description, Integer level, Integer id) {
this.id = id;
this.level = level;
this.description = description;
}
public int getId() {
return id;
}
public int getLevel() {
return level;
}
public String getDescription() {
return description;
}
#Override
public int hashCode() {
int hash = 7;
return hash;
}
public boolean contentEquals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PayString other = (PayString) obj;
if (!Objects.equals(this.description, other.description)) {
return false;
}
return true;
}
#Override
public String toString() {
return description;
}
}
`
You can always access the current data associated with your cell by calling
PayString value = getTreeItem().getValue()
However there is no 1:1 relationship between a cell and its value. JavaFX creates as many TreeCells it needs to display. It then dynamically assigns them to the underlying data items by calling the method
public void updateItem(PayString item, boolean empty) { ... }
This is where you can set a context menu that depends on the current data item. And there you get the data item passed in as parameter anyway.
At the code position you marked current data item will be null.

Resources