We try to achieve the following:
When a node gets selected in a JavaFX TreeTableView, also "the path to the root", i.e., the parent, the grandparent, and so on should get selected. Selected in this case means highlighted with a different background color, see the image (in the example, the node on Level 2 has been clicked by the user).
Is there a built-in function how to achieve this?
We tried using CSS but did not succeed.
There's no "built-in function" to do this. Use a row factory on the tree table view to create rows that observe the selected item, and set a pseudoclass on the row accordingly.
For example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectionPath extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<Item>();
PseudoClass ancestorOfSelection = PseudoClass.getPseudoClass("ancestor-of-selection");
table.setRowFactory(ttv -> new TreeTableRow<Item>() {
{
table.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> updateStyleClass());
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
updateStyleClass();
}
private void updateStyleClass() {
pseudoClassStateChanged(ancestorOfSelection, false);
TreeItem<Item> treeItem = table.getSelectionModel().getSelectedItem();
if (treeItem != null) {
for (TreeItem<Item> parent = treeItem.getParent() ; parent != null ; parent = parent.getParent()) {
if (parent == getTreeItem()) {
pseudoClassStateChanged(ancestorOfSelection, true);
break ;
}
}
}
}
});
TreeTableColumn<Item, String> itemCol = new TreeTableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getValue().getName()));
table.getColumns().add(itemCol);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
table.getColumns().add(valueCol);
table.setRoot(createRandomTree());
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree() {
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", 0));
Random rng = new Random();
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= 20 ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final String name ;
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
this.name = name ;
setValue(value);
}
public String getName() {
return name ;
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Now you can just style the "ancestor of a selected node" in CSS:
File style.css:
.tree-table-row-cell:ancestor-of-selection {
-fx-background: -fx-selection-bar;
-fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
}
(You may want to modify the CSS to get better control, e.g. set different colors for selected rows in a non-focused table, etc. See the default stylesheet for details on the default style.)
Here's a screenshot of the above test app:
Related
I currently have the following object data structure:
Item
String name
ArrayList of information
Character
String name
Collection of Item
Account
String name
Collection of Character (up to 8 max)
I want to make a TreeView that looks like the following:
Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)
I made all those names up and such, obviously the real implementation would be a lot more complex. How can this be done? The TreeItem requires each TreeItem to be the same type as its' parent.
The ONLY solution I have is to do the following:
public class ObjectPointer
{
Object pointer;
String name;
}
My TreeView would be of type ObjectPointer and on each row I would cast the ObjectPointer to Account, Character, or Item. This is AWFUL but I think it would work.
Sub Questions:
How do I get TreeItem(s) to detect setOnMouseHover events?
How do I get TreeItem(s) to not use the toString method of their type and instead a custom way of displaying the String property that they need?
How do I get the TreeItem(s) to display colored text in the GUI instead of plain text?
Thank you!
If you look at your model and think generically, all the classes have a degree of similarity, which you could factor out into a superclass:
package model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public abstract class GameObject<T extends GameObject<?>> {
public GameObject(String name) {
setName(name);
}
private final StringProperty name = new SimpleStringProperty();
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
private final ObservableList<T> items = FXCollections.observableArrayList();
public ObservableList<T> getItems() {
return items ;
}
public abstract void createAndAddChild(String name);
}
The type parameter T here represents the type of "child" objects. So your Account class (whose child type is GameCharacter - don't name classes the same as anything in java.lang, btw...) looks like
package model;
public class Account extends GameObject<GameCharacter> {
public Account(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new GameCharacter(name));
}
}
and similarly all the way down the hierarchy. I'd define an Information class (even though it just has a name) to make everything fit the structure, so:
package model;
public class Item extends GameObject<Information> {
public Item(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Information(name));
}
}
and, since Information has no children, its child list is just an empty list:
package model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Information extends GameObject<GameObject<?>> {
public Information(String name) {
super(name);
}
#Override
public ObservableList<GameObject<?>> getItems() {
return FXCollections.emptyObservableList();
}
#Override
public void createAndAddChild(String name) {
throw new IllegalStateException("Information has no child items");
}
}
Now every item in your tree is a GameObject<?>, so you can basically build a TreeView<GameObject<?>>. The tricky part is that your tree items need to reflect the structure already built in the model. Since you have observable lists there, you can do this with listeners on the lists.
You can use a cell factory on the tree to customize the appearance of the cells displaying the TreeItems. If you want a different appearance for each type of item, I'd recommend defining the styles in an external CSS class, and setting a CSS PseudoClass on the cell corresponding to the type of item. If you use some naming convention (I have that the pseudo-class name is the lower case version of the class name), it can be quite slick to do that. Here's a fairly simple example:
package ui;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
public class Tree {
private final TreeView<GameObject<?>> treeView ;
private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
Account.class, GameCharacter.class, Item.class, Information.class
);
public Tree(ObservableList<Account> accounts) {
treeView = new TreeView<>();
GameObject<?> root = new GameObject<Account>("") {
#Override
public ObservableList<Account> getItems() {
return accounts ;
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Account(name));
}
};
TreeItem<GameObject<?>> treeRoot = createItem(root);
treeView.setRoot(treeRoot);
treeView.setShowRoot(false);
treeView.setCellFactory(tv -> {
TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {
#Override
protected void updateItem(GameObject<?> item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty) {
setText(null);
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, false));
} else {
textProperty().bind(item.nameProperty());
PseudoClass itemPC = asPseudoClass(item.getClass());
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && (! cell.isEmpty())) {
System.out.println("Mouse hover on "+cell.getItem().getName());
}
});
return cell ;
}
}
public TreeView<GameObject<?>> getTreeView() {
return treeView ;
}
private TreeItem<GameObject<?>> createItem(GameObject<?> object) {
// create tree item with children from game object's list:
TreeItem<GameObject<?>> item = new TreeItem<>(object);
item.setExpanded(true);
item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));
// update tree item's children list if game object's list changes:
object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
while (c.next()) {
if (c.wasAdded()) {
item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
}
if (c.wasRemoved()) {
item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
}
}
});
return item ;
}
private PseudoClass asPseudoClass(Class<?> clz) {
return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
}
}
Quick test, which works, but note you probably need to test more of the functionality:
package application;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Tree tree = new Tree(createAccounts());
TreeView<GameObject<?>> treeView = tree.getTreeView();
TextField addField = new TextField();
Button addButton = new Button("Add");
EventHandler<ActionEvent> addHandler = e -> {
TreeItem<GameObject<?>> selected = treeView
.getSelectionModel()
.getSelectedItem();
if (selected != null) {
selected.getValue().createAndAddChild(addField.getText());
addField.clear();
}
};
addField.setOnAction(addHandler);
addButton.setOnAction(addHandler);
addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
return selected == null || selected.getValue() instanceof Information ;
}, treeView.getSelectionModel().selectedItemProperty()));
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(e -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
TreeItem<GameObject<?>> parent = selected.getParent() ;
parent.getValue().getItems().remove(selected.getValue());
});
deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());
HBox controls = new HBox(5, addField, addButton, deleteButton);
controls.setPadding(new Insets(5));
controls.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(treeView);
root.setBottom(controls);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private ObservableList<Account> createAccounts() {
Account jake = new Account("Jake");
Account mark = new Account("Mark");
Account freshAcc = new Account("Fresh Account");
Account marksAltAcc = new Account("Mark's alternative account");
Account jeffrey = new Account("Jeffrey");
GameCharacter jakesChar = new GameCharacter("Jakes character");
Item amazingSword = new Item("Amazing Sword");
Item brokenBow = new Item("Broken Bow");
Item junkMetal = new Item("Junk Metal");
GameCharacter myChar = new GameCharacter("Me");
Item godlyAxe = new Item("Godly Axe");
GameCharacter level = new GameCharacter("I'll level this I promise");
GameCharacter jeff = new GameCharacter("Jeff");
Item superGun = new Item("Super Gun");
Item superGunScope = new Item("Super Gun Scope");
jake.getItems().add(jakesChar);
mark.getItems().add(myChar);
marksAltAcc.getItems().add(level);
jeffrey.getItems().add(jeff);
jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
myChar.getItems().add(godlyAxe);
jeff.getItems().addAll(superGun, superGunScope);
return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);
}
}
and the CSS as an example:
.tree-cell, .tree-cell:hover:empty {
-fx-background-color: -fx-background ;
-fx-background: -fx-control-inner-background ;
}
.tree-cell:hover {
-fx-background-color: crimson, -fx-background ;
-fx-background-insets: 0, 1;
}
.tree-cell:account {
-fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
-fx-background: bisque ;
}
.tree-cell:item {
-fx-background: antiquewhite ;
}
.tree-cell:selected {
-fx-background: crimson ;
}
I'm trying to select all child check boxes from a parent root. The action is invoked when the parent check box is selected.
Here's the pseudo/modified/shortened set up with scenebuilder:
#FXML
private TreeTableView<Info> testTable;
#FXML
private TreeTableColumn<Info, Boolean> checkBoxCol;
Model:
public class Info{
private final BooleanProperty onHold;
public Info(){
this.onHold = new SimpleBooleanProperty(false);
}
public Boolean getOnHold(){
return onHold.get();
}
public void setOnHold(Boolean onHold) {
this.onHold.set(onHold);
}
public BooleanProperty onHoldProperty(){
return onHold;
}
}
Controller:
checkBoxCol.setCellValueFactory(new TreeItemPropertyValueFactory("onHold"));
checkBoxCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(checkBoxCol));
End Result would look like this (when parent nodes are clicked):
I tried onEditCommit/start/cancel, but those seem to only affect the cell and not the checkboxes. I am not exactly sure how to get a listener for only the parent nodes so that it can check all values underneath (if they have children). If it's too difficult to only allow just the parent nodes to have the listener, then all the checkbox can have listeners and I can simply check if there are children with:node.getChildren().size()
You should be able to manage this entirely in the model.
The TreeTableView consists of a TreeItem<Info> root with a bunch of descendent nodes. Just arrange that whenever you create the tree items, you add a listener to the properties:
private TreeItem<Info> createTreeItem(Info info) {
TreeItem<Info> item = new TreeItem<>(info);
info.onHoldProperty().addListener((obs, wasOnHold, isNowOnHold) -> {
if (isNowOnHold) {
item.getChildren().forEach(child -> child.getValue().setOnHold(true));
}
});
return item ;
}
Complete example:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeTableViewInheritableCheckBoxes extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Info> table = new TreeTableView<>();
table.setEditable(true);
TreeTableColumn<Info, Boolean> infoCol = new TreeTableColumn<>("Info");
infoCol.setPrefWidth(200);
infoCol.setCellValueFactory(cellData -> cellData.getValue().getValue().onHoldProperty());
infoCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(infoCol));
table.getColumns().add(infoCol);
TreeItem<Info> root = createTreeItem(new Info());
buildTree(root, 0);
table.setRoot(root);
primaryStage.setScene(new Scene(new BorderPane(table), 250, 400));
primaryStage.show();
}
private void buildTree(TreeItem<Info> parent, int depth) {
if (depth > 2) return ;
for (int i = 0; i < 5; i++) {
TreeItem<Info> item = createTreeItem(new Info());
parent.getChildren().add(item);
buildTree(item, depth + 1);
}
}
private TreeItem<Info> createTreeItem(Info info) {
TreeItem<Info> item = new TreeItem<>(info);
info.onHoldProperty().addListener((obs, wasOnHold, isNowOnHold) -> {
if (isNowOnHold) {
item.getChildren().forEach(child -> child.getValue().setOnHold(true));
}
});
return item ;
}
public static class Info {
private final BooleanProperty onHold;
public Info(){
this.onHold = new SimpleBooleanProperty(false);
}
public Boolean getOnHold(){
return onHold.get();
}
public void setOnHold(Boolean onHold) {
this.onHold.set(onHold);
}
public BooleanProperty onHoldProperty(){
return onHold;
}
}
public static void main(String[] args) {
launch(args);
}
}
How can I display the names of nodes with children bold in a javax TreeView? (Leave nodes should be displayed non-bold)
Use a cell factory on the tree that sets the state of a CSS pseudoclass on the tree cell it creates, according to whether the tree item displayed is a leaf or not. Then you can use an external css file that styles the leaf nodes and non-leaf nodes any way you like.
Example:
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class BoldNonLeafNodes extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final BorderPane uiRoot = new BorderPane();
TreeItem<Integer> root = createTreeItem(1);
final TreeView<Integer> tree = new TreeView<>(root);
PseudoClass leaf = PseudoClass.getPseudoClass("leaf");
tree.setCellFactory(tv -> {
TreeCell<Integer> cell = new TreeCell<>();
cell.itemProperty().addListener((obs, oldValue, newValue) -> {
if (newValue == null) {
cell.setText("");
} else {
cell.setText(newValue.toString());
}
});
cell.treeItemProperty().addListener((obs, oldTreeItem, newTreeItem) ->
cell.pseudoClassStateChanged(leaf, newTreeItem != null && newTreeItem.isLeaf()));
return cell ;
});
uiRoot.setCenter(tree);
final Scene scene = new Scene(uiRoot);
scene.getStylesheets().add("bold-non-leaf-nodes.css");
primaryStage.setScene(scene);
primaryStage.setTitle(getClass().getSimpleName());
primaryStage.show();
}
private TreeItem<Integer> createTreeItem(int value) {
TreeItem<Integer> item = new TreeItem<>(value);
if (value < 10000) {
for (int i=0; i<10; i++) {
item.getChildren().add(createTreeItem(10*value+i));
}
}
return item ;
}
public static void main(String[] args) {
launch(args);
}
}
bold-non-leaf-nodes.css:
.tree-cell {
-fx-font-weight: bold ;
}
.tree-cell:leaf {
-fx-font-weight: normal ;
}
I turned my column names as suggested here to labels so that I can get tooltip on their names:
for (Entry<String, String> ent : dc.getSortedAssignedOrg().entrySet()) {
TreeTableColumn<String, ArrayList<String>> col = new TreeTableColumn<>();
Label label = new Label(ent.getValue());
col.setGraphic(label);
col.setEditable(false);
col.setSortable(false);
label.setTooltip(new Tooltip(label.getText()));// tooltip for column
.
.
.
Now the problem is my TableMenuButton does not show the column names, and clicking on the plus sign on the right corner of treetableview opens a list in which there are only the checked signs, which I can remove or add. But the name itself is not shown. How can I fix this?
You could create your own table menu. You're better off with a custom menu anyway, since the in-built menu e. g. closes each time you click on a button. Unfortunately there is no getter for the context menu, so you'll have to find access to it either via reflection or a lookup.
I created a gist for a custom menu via the reflection and the lookup mechanism. Maybe it's of help for you.
The relevant part for you would be
CheckBox cb = new CheckBox(tableColumn.getText());
where you set the menu item to the text of your preferrence, i. e. the text of your labels.
Here's a lookup version for TreeTableView:
CustomTreeTableMenuDemo.java
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class CustomTreeTableMenuDemo extends Application {
List<Employee> employees = Arrays.<Employee> asList(new Employee(
"Ethan Williams", "ethan.williams#example.com"), new Employee(
"Emma Jones", "emma.jones#example.com"), new Employee(
"Michael Brown", "michael.brown#example.com"), new Employee(
"Anna Black", "anna.black#example.com"), new Employee(
"Rodger York", "roger.york#example.com"), new Employee(
"Susan Collins", "susan.collins#example.com"));
final TreeItem<Employee> root = new TreeItem<>(new Employee(
"Sales Department", ""));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Table Menu Demo");
stage.setWidth(500);
stage.setHeight(550);
root.setExpanded(true);
employees.stream().forEach((employee) -> {
root.getChildren().add(new TreeItem<>(employee));
});
stage.setTitle("Tree Table View Sample");
final Scene scene = new Scene(new Group(), 400, 400);
scene.setFill(Color.LIGHTGRAY);
Group sceneRoot = (Group) scene.getRoot();
TreeTableColumn<Employee, String> empColumn = new TreeTableColumn<>(
"Employee");
empColumn.setPrefWidth(150);
empColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<Employee, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getName()));
TreeTableColumn<Employee, String> emailColumn = new TreeTableColumn<>(
"Email");
emailColumn.setPrefWidth(190);
emailColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<Employee, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getEmail()));
TreeTableView<Employee> treeTableView = new TreeTableView<>(root);
treeTableView.getColumns().setAll(empColumn, emailColumn);
sceneRoot.getChildren().add(treeTableView);
stage.setScene(scene);
stage.show();
// enable table menu button and add a custom menu to it
TreeTableUtils.addCustomTreeTableMenu(treeTableView);
}
public class Employee {
private SimpleStringProperty name;
private SimpleStringProperty email;
public SimpleStringProperty nameProperty() {
if (name == null) {
name = new SimpleStringProperty(this, "name");
}
return name;
}
public SimpleStringProperty emailProperty() {
if (email == null) {
email = new SimpleStringProperty(this, "email");
}
return email;
}
private Employee(String name, String email) {
this.name = new SimpleStringProperty(name);
this.email = new SimpleStringProperty(email);
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
TreeTableUtils.java
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TreeTableViewSkin;
public class TreeTableUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param treeTableView
*/
public static void addCustomTreeTableMenu( TreeTableView treeTableView) {
// enable table menu
treeTableView.setTableMenuButtonVisible(true);
// replace internal mouse listener with custom listener
setCustomContextMenu( treeTableView);
}
private static void setCustomContextMenu( TreeTableView treeTableView) {
TreeTableViewSkin<?> treeTableViewSkin = (TreeTableViewSkin<?>) treeTableView.getSkin();
// get all children of the skin
ObservableList<Node> children = treeTableViewSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
TableHeaderRow tableHeaderRow = (TableHeaderRow) node;
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
for( Node child: tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if( child.getStyleClass().contains( "show-hide-columns-button")) {
// get the context menu
ContextMenu columnPopupMenu = createContextMenu( treeTableView);
// replace mouse listener
child.setOnMousePressed(me -> {
// show a popupMenu which lists all columns
columnPopupMenu.show(child, Side.BOTTOM, 0, 0);
me.consume();
});
}
}
}
}
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param treeTableView
*/
private static ContextMenu createContextMenu( TreeTableView treeTableView) {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
// select all item
Label showAll = new Label("Show all");
showAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : treeTableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(showAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label hideAll = new Label("Hide all");
hideAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : treeTableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(hideAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : treeTableView.getColumns()) {
TreeTableColumn<?, ?> tableColumn = (TreeTableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
}
I have a ListView with a TextField above it. If a user enters in a search query into the textfield, the listview will update and filter itself to show relevant results.
The ListView shows items from a FilteredList, which is filled with Employee objects. Each Employee has a first and last name.
package application.ctrl;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import application.Main;
import application.objects.Employee;
import application.objects.EmployeeDatabase;
public class EmployeePickerWidget extends VBox implements Initializable {
#FXML
private TextField textField;
#FXML
private Button addNewEmployee;
#FXML
private ListView<Employee> employeeList;
private FilteredList<Employee> filteredList;
private ContextMenu cm;
private CustomMenuItem item;
private ClickedEmployeeInterface parent;
public EmployeePickerWidget(ClickedEmployeeInterface parent) {
FXMLLoader loader = new FXMLLoader(this.getClass().getResource(
Main.EMPLOYEE_PICKER));
loader.setRoot(this);
loader.setController(this);
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
this.parent = parent;
}
#Override
public void initialize(URL location, ResourceBundle resources) {
setupEmployeeListView();
setupTextField();
}
private void setupEmployeeListView() {
filteredList = new FilteredList<Employee>(EmployeeDatabase.getInstance()
.getObservableList());
employeeList = new ListView<Employee>();
employeeList.setItems(filteredList);
employeeList.setOnMouseClicked(arg0 -> {
if (employeeList.getSelectionModel().getSelectedItem() != null) {
cm.hide();
parent.handleClickedEmployee();
}
});
}
private void setupTextField() {
textField.textProperty().addListener(
(observable, oldValue, newValue) -> {
filteredList.setPredicate(employee -> {
return filterHelper(employee, newValue);
});
});
textField.setText(" ");
textField.setText("");
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
cm = new ContextMenu();
item = new CustomMenuItem();
VBox container = new VBox();
container.setAlignment(Pos.CENTER_RIGHT);
container.getChildren().add(employeeList);
Button defineEmployeeBtn = new Button("Define New Employee");
defineEmployeeBtn.setOnAction(event -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
Main.DEFINE_NEW_EMPLOYEE));
Parent root = null;
try {
root = loader.load();
} catch (IOException e) {
e.printStackTrace();
}
Scene newScene = new Scene(root);
Stage newStage = new Stage();
newStage.setScene(newScene);
newStage.show();
});
container.getChildren().add(defineEmployeeBtn);
item.setContent(container);
cm.getItems().add(item);
}
private boolean filterHelper(Employee employee, String query) {
String first = employee.getFirst().toLowerCase(), last = employee
.getLast().toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
if (length == 1) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
return true;
else
return false;
} else if (length == 2) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
if (first.contains(querySplit[1]) || last.contains(querySplit[1]))
return true;
return false;
} else if (length == 3) {
return false;
}
return false;
}
public Employee getEmployee() {
return employeeList.getSelectionModel().getSelectedItem();
}
#FXML
public void addNewEmployee() {
}
}
interface ClickedEmployeeInterface {
void handleClickedEmployee();
}
If there were 3 employees named "Donald Trump", "Donald Smith", and "Donald Jackson" in the database, then the following needs to happen:
Typing up to the word "Donald" will show all 3 results.
Typing a space after Donald (resulting in "Donald ") will still show 3 results.
Typing a T after the previous query (resulting in "Donald T") should only show 1 result.
The problem is, after I enter in a space, the ListView breaks, and all of my Employees disappear from the ListView. When I click outside of the textfield and click back in again, it triggers this:
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
And my ListView suddenly works again, showing that one Employee.
How do I make the ListView filter properly without having to click out and back in?
I do not have the FXML file, so I wasn't able to replicate your problem. There are multiple problems with your code and this is the not the optimum solution, still, I have edited your answer to give you hints and help you understand the areas where you might have committed logical errors
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class DemoList extends Application {
#Override
public void start(Stage stage) throws Exception {
GridPane gridPane = new GridPane();
Label label = new Label("Name");
final TextField textField = new TextField();
textField.setFocusTraversable(false);
textField.setPromptText("Please Type Here");
final ContextMenu cm = new ContextMenu();
final ObservableList<String> employeeList = FXCollections
.observableArrayList();
employeeList.addAll("Donald Duck", "Donald Mouse", "Donald Goofy");
textField.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> arg0,
String arg1, String arg2) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
for (String employee : employeeList) {
if (filterHelper(employee, arg2)) {
cm.getItems().add(new MenuItem(employee));
}
}
}
});
textField.setOnMouseClicked(new EventHandler<Event>() {
#Override
public void handle(Event arg0) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
//Adding the data for initial click
for (String employee : employeeList) {
if (filterHelper(employee, textField.getText())) {
cm.getItems().add(new MenuItem(employee));
}
}
cm.show(textField, Side.BOTTOM, 0, 0);
}
});
gridPane.add(label, 0, 0);
gridPane.add(textField, 0, 1);
Scene scene = new Scene(gridPane, 300, 300);
stage.setScene(scene);
stage.show();
}
private boolean filterHelper(String employee, String query) {
//Splitting Employee name to fetch first and last name
String first = employee.split(" ")[0].toLowerCase(), last = employee
.split(" ")[1].toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
/**
* Avoid adding unnecessary return statement
* I have removed all the 'return false' statements
* The last return will take care of all the 'return false'
*/
//only single word
if (length == 1) {
if (first.startsWith(querySplit[0])
|| last.startsWith(querySplit[0]))
return true;
}
//two words, considering first word is first name
//and second word is last name
else if (length == 2) {
if (first.startsWith(querySplit[0])
&& last.startsWith(querySplit[1]))
return true;
}
return false;
}
public static void main(String[] args) {
launch(args);
}
}