JavaFX: How to highlight certain Items in a TreeView - javafx

I am trying to implement a search function for a TreeView in JavaFX. I want to highlight all the matches when the user hits the enter key. So I added a boolean isHighlighted to my TreeItem and in my TreeCells updateItem, I check whether the item isHighlighted and if so I apply a certain CSS. Everything works fine with the items/cells not visible at the moment of the search -- when I scroll to them, they are properly highlighted. The problem is: How can I "repaint" the TreeCells visible at search so that they reflect whether their item isHighlighted? My Controller does currently not have any reference to the TreeCells the TreeView creates.

This answer is based on this one, but adapted for TreeView instead of TableView, and updated to use JavaFX 8 functionality (greatly reducing the amount of code required).
One strategy for this is to maintain an ObservableSet of TreeItems that match the search (this is sometimes useful for other functionality you may want anyway). Use a CSS PseudoClass and an external CSS file to highlight the required cells. You can create a BooleanBinding in the cell factory that binds to the cell's treeItemProperty and the ObservableSet, evaluating to true if the set contains the cell's current tree item. Then just register a listener with the binding and update the pseudoclass state of the cell when it changes.
Here's a SSCCE. It contains a tree whose items are Integer-valued. It will update the search when you type in the search box, matching those whose value is a multiple of the value entered.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
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 TreeWithSearchAndHighlight extends Application {
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>(createRandomTree(100));
// keep track of items that match our search:
ObservableSet<TreeItem<Integer>> searchMatches = FXCollections.observableSet(new HashSet<>());
// cell factory returns an instance of TreeCell implementation defined below.
// pass the cell implementation a reference to the set of search matches
tree.setCellFactory(tv -> new SearchHighlightingTreeCell(searchMatches));
// search text field:
TextField textField = new TextField();
// allow only numeric input:
textField.setTextFormatter(new TextFormatter<Integer>(change ->
change.getControlNewText().matches("\\d*")
? change
: null));
// when the text changes, update the search matches:
textField.textProperty().addListener((obs, oldText, newText) -> {
// clear search:
searchMatches.clear();
// if no text, or 0, just exit:
if (newText.isEmpty()) {
return ;
}
int searchValue = Integer.parseInt(newText);
if (searchValue == 0) {
return ;
}
// search for matching nodes and put them in searchMatches:
Set<TreeItem<Integer>> matches = new HashSet<>();
searchMatchingItems(tree.getRoot(), matches, searchValue);
searchMatches.addAll(matches);
});
BorderPane root = new BorderPane(tree, textField, null, null, null);
BorderPane.setMargin(textField, new Insets(5));
BorderPane.setMargin(tree, new Insets(5));
Scene scene = new Scene(root, 600, 600);
// stylesheet sets style for cells matching search by using the selector
// .tree-cell:search-match
// (specified in the initalization of the Pseudoclass at the top of the code)
scene.getStylesheets().add("tree-highlight-search.css");
primaryStage.setScene(scene);
primaryStage.show();
}
// find all tree items whose value is a multiple of the search value:
private void searchMatchingItems(TreeItem<Integer> searchNode, Set<TreeItem<Integer>> matches, int searchValue) {
if (searchNode.getValue() % searchValue == 0) {
matches.add(searchNode);
}
for (TreeItem<Integer> child : searchNode.getChildren()) {
searchMatchingItems(child, matches, searchValue);
}
}
// build a random tree with numNodes nodes (all nodes expanded):
private TreeItem<Integer> createRandomTree(int numNodes) {
List<TreeItem<Integer>> items = new ArrayList<>();
TreeItem<Integer> root = new TreeItem<>(1);
root.setExpanded(true);
items.add(root);
Random rng = new Random();
for (int i = 2 ; i <= numNodes ; i++) {
TreeItem<Integer> item = new TreeItem<>(i);
item.setExpanded(true);
TreeItem<Integer> parent = items.get(rng.nextInt(items.size()));
parent.getChildren().add(item);
items.add(item);
}
return root ;
}
public static class SearchHighlightingTreeCell extends TreeCell<Integer> {
// must keep reference to binding to prevent premature garbage collection:
private BooleanBinding matchesSearch ;
public SearchHighlightingTreeCell(ObservableSet<TreeItem<Integer>> searchMatches) {
// pseudoclass for highlighting state
// css can set style with selector
// .tree-cell:search-match { ... }
PseudoClass searchMatch = PseudoClass.getPseudoClass("search-match");
// initialize binding. Evaluates to true if searchMatches
// contains the current treeItem
// note the binding observes both the treeItemProperty and searchMatches,
// so it updates if either one changes:
matchesSearch = Bindings.createBooleanBinding(() ->
searchMatches.contains(getTreeItem()),
treeItemProperty(), searchMatches);
// update the pseudoclass state if the binding value changes:
matchesSearch.addListener((obs, didMatchSearch, nowMatchesSearch) ->
pseudoClassStateChanged(searchMatch, nowMatchesSearch));
}
// update the text when the item displayed changes:
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : "Item "+item);
}
}
public static void main(String[] args) {
launch(args);
}
}
The CSS file tree-highlight-search.css just has to contain a style for the highlighted cells:
.tree-cell:search-match {
-fx-background: yellow ;
}

Related

JavaFx table view: block selection events but only the ones coming from the user interaction

I have been trying to create a javafx.scene.control.TableView such that all the selection events are blocked when their origin is user interaction. In other words, it must be possible for me to programmatically alter the selection in a given table view.
I tried solutions from the following questions:
Setting the whole table view as mouse transparent (see article). This approach is unacceptable, because, for instance, user cannot change the width of the columns
Setting the selection model to null (see article). This one is unacceptable, because the currently selected row is not highlighted properly- see image below:
Originally, I wanted to decorate the default existing table view selection model with my own. Something like this was created:
private final class TableViewSelectionModelDecorator< S >extends TableViewSelectionModel< S >
{
private final TableViewSelectionModel< S > delegate;
private TableViewSelectionModelDecorator( TableViewSelectionModel< S > aDelegate )
{
super( aDelegate.getTableView() );
delegate = Objects.requireNonNull( aDelegate );
}
// Overriding the methods and delegating the calls to the delegate
}
The problem with my decorator is that the function getSelectedIndex() from the selection model is marked as final, which means I cannot override it and delegate the call to my decorated selection model. As a result, whenever a client asks for currently selected index the result is -1.
Requirements that I must meet:
Selection change events coming from either the mouse click or the keyboard (or any other input source) is blocked.
User must be able to interact with the table as long as the selection is not modified (e.g. changing the width of the columns)
Selected entry is properly highlighted (instead of just some frame around the selected index)
For now there is no multiselection support involved, but preferably I'd appreciate a solution that does support it.
Last note is I use Java 11.
Thanks for any pointers.
Please do consider the comments mentioned about the xy problem and other alternatives mentioned.
If you still want to solve this as the way you mentioned, you can give a try as below.
The idea is to
block all KEY_PRESSED events on tableView level and
set mouse transparent on tableRow level
so that we are not tweaking with any default selection logic. This way you can still interact with columns and scrollbar using mouse.
Below is the quick demo of the implementation:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableViewSelectionBlockingDemo extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 1; i < 11; i++) {
persons.add(new Person(i + "", "A" + i));
}
TableView<Person> tableView = new TableView<>();
TableColumn<Person, String> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(param -> param.getValue().idProperty());
idCol.setPrefWidth(100);
TableColumn<Person, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(param -> param.getValue().nameProperty());
nameCol.setPrefWidth(150);
tableView.getColumns().addAll(idCol,nameCol);
tableView.setItems(persons);
// Selection Blocking logic
tableView.addEventFilter(KeyEvent.KEY_PRESSED, e->e.consume());
tableView.setRowFactory(personTableView -> new TableRow<Person>(){
{
setMouseTransparent(true);
}
});
ComboBox<Integer> comboBox = new ComboBox<>();
for (int i = 1; i < 11; i++) {
comboBox.getItems().add(i);
}
comboBox.valueProperty().addListener((obs, old, val) -> {
if (val != null) {
tableView.getSelectionModel().select(val.intValue()-1);
} else {
tableView.getSelectionModel().clearSelection();
}
});
HBox row = new HBox(new Label("Select Row : "), comboBox);
row.setSpacing(10);
VBox vb = new VBox(row, tableView);
vb.setSpacing(10);
vb.setPadding(new Insets(10));
VBox.setVgrow(tableView, Priority.ALWAYS);
Scene scene = new Scene(vb, 500, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("TableView Selection Blocking Demo");
primaryStage.show();
}
class Person {
private StringProperty name = new SimpleStringProperty();
private StringProperty id = new SimpleStringProperty();
public Person(String id1, String name1) {
name.set(name1);
id.set(id1);
}
public StringProperty nameProperty() {
return name;
}
public StringProperty idProperty() {
return id;
}
}
}
Note: This may not be the approach for editable table.

javafx combobox checkbox multiselect filtered

I have looked days on any ready solution for the subject of having TOGETHER in javafx (pure) :
Combobox
Multiselect of items through Checkboxes
Filter items by the "editable" part of the Combobox
I have had no luck finding what I was looking for so I have now a working solution taken from different separate solution... Thank you to all for this !
Now I would like to know if what I have done follows the best practices or not... It's working... but is it "ugly" solution ? Or would that be a sort of base anyone could use ?
I tied to comment as much as I could, and also kept the basic comment of the sources :
user:2436221 (Jonatan Stenbacka) --> https://stackoverflow.com/a/34609439/14021197
user:5844477 (Sai Dandem) --> https://stackoverflow.com/a/52471561/14021197
Thank you for your opinions, and suggestions...
Here is the working example :
package application;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;
#SuppressWarnings ("restriction") // Only applies for PROTECTD library : com.sun.javafx.scene.control.skin.ComboBoxListViewSkin
public class MultiSelectFiltered2 extends Application {
// These 2 next fields are used in order to keep the FILTERED TEXT entered by user.
private String aFilterText = "";
private boolean isUserChangeText = true;
public void start(Stage stage) {
Text txt = new Text(); // A place where to expose the result of checked items.
HBox vbxRoot = new HBox(); // A basic root to order the GUI
ComboBox<ChbxItems> cb = new ComboBox<ChbxItems>() {
// This part is needed in order to NOT have the list hided when an item is selected...
// TODO --> Seems a little ugly to me since this part is the PROTECTED part !
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new ComboBoxListViewSkin<ChbxItems>(this) {
#Override
protected boolean isHideOnClickEnabled() {
return false;
}
};
}
};
cb.setEditable(true);
// Create a list with some dummy values.
ObservableList<ChbxItems> items = FXCollections.observableArrayList();
items.add(new ChbxItems("One"));
items.add(new ChbxItems("Two"));
items.add(new ChbxItems("Three"));
items.add(new ChbxItems("Four"));
items.add(new ChbxItems("Five"));
items.add(new ChbxItems("Six"));
items.add(new ChbxItems("Seven"));
items.add(new ChbxItems("Eight"));
items.add(new ChbxItems("Nine"));
items.add(new ChbxItems("Ten"));
// Create a FilteredList wrapping the ObservableList.
FilteredList<ChbxItems> filteredItems = new FilteredList<ChbxItems>(items, p -> true);
// Add a listener to the textProperty of the combo box editor. The
// listener will simply filter the list every time the input is changed
// as long as the user hasn't selected an item in the list.
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
// This needs to run on the GUI thread to avoid the error described here:
// https://bugs.openjdk.java.net/browse/JDK-8081700.
Platform.runLater(() -> {
if (isUserChangeText) {
aFilterText = cb.getEditor().getText();
}
// If the no item in the list is selected or the selected item
// isn't equal to the current input, we re-filter the list.
filteredItems.setPredicate(item -> {
boolean isPartOfFilter = true;
// We return true for any items that starts with the
// same letters as the input. We use toUpperCase to
// avoid case sensitivity.
if (!item.getText().toUpperCase().startsWith(newValue.toUpperCase())) {
isPartOfFilter = false;
}
return isPartOfFilter;
});
isUserChangeText = true;
});
});
cb.setCellFactory(new Callback<ListView<ChbxItems>, ListCell<ChbxItems>>() {
#Override
public ListCell<ChbxItems> call(ListView<ChbxItems> param) {
return new ListCell<ChbxItems>() {
private CheckBox chbx = new CheckBox();
// This 'just open bracket' opens the newly CheckBox Class specifics
{
chbx.setOnAction(new EventHandler<ActionEvent>() {
// This VERY IMPORTANT part will effectively set the ChbxItems item
// The argument is never used, thus left as 'arg0'
#Override
public void handle(ActionEvent arg0) {
// This is where the usual update of the check box refreshes the editor' text of the parent combo box... we want to avoid this ;-)
isUserChangeText = false;
// The one line without which your check boxes are going to be checked depending on the position in the list... which changes when the list gets filtered.
getListView().getSelectionModel().select(getItem());
// Updating the exposed text from the list of checked items... This is added here to have a 'live' update.
txt.setText(updateListOfValuesChosen(items));
}
});
}
private BooleanProperty booleanProperty; //Will be used for binding... explained bellow.
#Override
protected void updateItem(ChbxItems item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
// Binding is used in order to link the checking (selecting) of the item, with the actual 'isSelected' field of the ChbxItems object.
if (booleanProperty != null) {
chbx.selectedProperty().unbindBidirectional(booleanProperty);
}
booleanProperty = item.isSelectedProperty();
chbx.selectedProperty().bindBidirectional(booleanProperty);
// This is the usual part for the look of the cell
setGraphic(chbx);
setText(item.getText() + "");
} else {
// Look of the cell, which has to be "reseted" if no item is attached (empty is true).
setGraphic(null);
setText("");
}
// Setting the 'editable' part of the combo box to what the USER wanted
// --> When 'onAction' of the check box, the 'behind the scene' update will refresh the combo box editor with the selected object reference otherwise.
cb.getEditor().setText(aFilterText);
cb.getEditor().positionCaret(aFilterText.length());
}
};
}
});
// Yes, it's the filtered items we want to show in the combo box...
// ...but we want to run through the original items to find out if they are checked or not.
cb.setItems(filteredItems);
// Some basic cosmetics
vbxRoot.setSpacing(15);
vbxRoot.setPadding(new Insets(25));
vbxRoot.setAlignment(Pos.TOP_LEFT);
// Adding the visual children to root VBOX
vbxRoot.getChildren().addAll(txt, cb);
// Ordinary Scene & Stage settings and initialization
Scene scene = new Scene(vbxRoot);
stage.setScene(scene);
stage.show();
}
// Just a method to expose the list of items checked...
// This is the result that will be probably the input for following code.
// -->
// If the class ChbxItems had a custom object rather than 'text' field,
// the resulting checked items from here could be a list of these custom objects --> VERY USEFUL
private String updateListOfValuesChosen(ObservableList<ChbxItems> items) {
StringBuilder sb = new StringBuilder();
items.stream().filter(ChbxItems::getIsSelected).forEach(cbitem -> {
sb.append(cbitem.getText()).append("\n");
});
return sb.toString();
}
// The CHECKBOX object, with 2 fields :
// - The boolean part (checked ot not)
// - The text part which is shown --> Could be a custom object with 'toString()' overridden ;-)
class ChbxItems {
private SimpleStringProperty text = new SimpleStringProperty();
private BooleanProperty isSelected = new SimpleBooleanProperty();
public ChbxItems(String sText) {
setText(sText);
}
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
public SimpleStringProperty textProperty() {
return text;
}
public void setIsSelected(boolean isSelected) {
this.isSelected.set(isSelected);
}
public boolean getIsSelected() {
return isSelected.get();
}
public BooleanProperty isSelectedProperty() {
return isSelected;
}
}
public static void main(String[] args) {
launch();
}
}

Create a tree with a label and a chekcbox,detect whether its parent is chekced

I want to create a treeview whose item has a lable and a checkbox on the left.
I try to write as below,but when I click the button,it prints null.If I can get the graphic not null,I can get the checkbox in it.
My purpose is to konw whether the checkbox of the parent item is checked.
package com.qy.tth.fxgui;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TreeCheck extends Application{
public static void main(String[] args) {
launch(null);
}
private TreeItem<String> item11;
#Override
public void start(Stage stage) throws Exception {
TreeItem<String> item1=new TreeItem<>("1");
TreeItem<String> item2=new TreeItem<>("2");
item11=new TreeItem<>("1-1");
TreeItem<String> itemRoot=new TreeItem<>("root");
item1.getChildren().add(item11);
itemRoot.getChildren().addAll(item1,item2);
TreeView<String> tv=new TreeView<>();
tv.setRoot(itemRoot);
tv.setCellFactory(tv1 -> new TreeCell<String>() {
private HBox hb;
{
Label lable = new Label("icon");
CheckBox cb=new CheckBox();
hb=new HBox();
hb.getChildren().addAll(lable,cb);
setGraphic(hb);
}
protected void updateItem(String value, boolean empty) {
super.updateItem(value, empty);
if (empty || value == null) {
setText("");
hb.setVisible(false);
}else{
setText(value);
hb.setVisible(true);
}
};
});
Button btn=new Button("show parent");
btn.setOnAction(e->showParent());
VBox vb=new VBox();
vb.getChildren().addAll(btn,tv);
Scene scene=new Scene(vb);
stage.setScene(scene);
stage.show();
}
private void showParent() {
TreeItem<String> item1 = item11.getParent();
Node graph = item1.getGraphic();
System.out.println(graph);
}
}
And I am not sure is it the best way to write like this,or you can give you own code completely.
My intent is just create a tree with a label and checkbox,then detect whether its parent is checked
The reason your code is printing null, I believe, is because there is no graphic set on the TreeItem. You set the graphic (an HBox) on the TreeCell and then later try to retrieve that graphic from the TreeItem.
That being said, if you are okay with the Label being to the right of the CheckBox there is no need to create your own TreeCell implementation. Instead, you can use the built in classes for this: CheckBoxTreeCell and CheckBoxTreeItem.
To set up the TreeView you'd use the following code:
TreeView<String> tree = new TreeView<>();
tree.setCellFactory(CheckBoxTreeCell.forTreeView());
The static method CheckBoxTreeCell.forTreeView() assumes that the root TreeItem and all descendants will be instances of CheckBoxTreeItem. Be default, the behavior of CheckBoxTreeCell is:
If no children are selected then the parent is not selected.
If some, but not all, children are selected then the parent is indeterminate.
If all children are selected then the parent is selected.
If you don't want this behavior you should set the independent property to true. This property states:
A BooleanProperty used to represent the independent state of this
CheckBoxTreeItem. The independent state is used to represent whether
changes to a single CheckBoxTreeItem should influence the state of its
parent and children.
By default, the independent property is false, which means that when a
CheckBoxTreeItem has state changes to the selected or indeterminate
properties, the state of related CheckBoxTreeItems will possibly be
changed. If the independent property is set to true, the state of
related CheckBoxTreeItems will never change.
Setting your TreeView up this way makes it easy to determine if a parent is selected because CheckBoxTreeItem has a BooleanProperty named selected.
CheckBoxTreeItem<?> parent = (CheckBoxTreeItem<?>) item.getParent();
System.out.println(parent.isSelected());
In this case, casting it okay because we know all the TreeItems will be instances of CheckBoxTreeItem.
Since you also want a Label as a part of the TreeCell you can simply set the graphic of the CheckBoxTreeItem to a Label with the desired text. Note, however, that this will put the Label to the right of the actual check box. If, as indicated in your code, you want the Label to the left of the CheckBox this gets a little more involved.
Internally, a CheckBoxTreeCell takes the graphic of the TreeItem and sets it on the internal CheckBox; then it sets the CheckBox as the graphic of itself. Due to the way CheckBox is designed it is not simple (probably not even possible) to put the graphic/text of the CheckBox on the other side of the actual check box (the visual box with the check). If you want to have the TreeItems graphic be on the left side of the CheckBox you have to use something like an HBox (as you were doing in your code). This can be done simpler than your attempt if you extend CheckBoxTreeCell rather than TreeCell directly.
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
public class CustomCheckBoxTreeCell<T> extends CheckBoxTreeCell<T> {
public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView() {
return treeView -> new CustomCheckBoxTreeCell<>();
}
private final InvalidationListener graphicListener = (obs) -> updateItem(getItem(), isEmpty());
private final WeakInvalidationListener weakGraphicListener = new WeakInvalidationListener(graphicListener);
public CustomCheckBoxTreeCell() {
// This again assumes that all TreeItems will actually be
// instances of CheckBoxTreeItem
super(item -> ((CheckBoxTreeItem<?>) item).selectedProperty());
treeItemProperty().addListener((observable, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.graphicProperty().removeListener(weakGraphicListener);
}
if (newItem != null) {
newItem.graphicProperty().addListener(weakGraphicListener);
}
});
}
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty && getTreeItem().getGraphic() != null) {
CheckBox cBox = (CheckBox) getGraphic();
cBox.setGraphic(null);
HBox hBox = new HBox(getTreeItem().getGraphic(), cBox);
hBox.setAlignment(Pos.CENTER_LEFT);
setGraphic(hBox);
}
}
}
You would then set the cell factory like so:
tree.setCellFactory(CustomCheckBoxTreeItem.forTreeView());
A few notes:
This relies on how CheckBoxTreeCell is implemented internally (specifically Java 10, but probably works with Java 8 as well). If they change the implementation in the future this could break.
This requires you to set the Label as the TreeItems graphic rather than using it in the TreeCell only. If you don't want to do this, then remove the behavior that uses the TreeItems graphic and replace it with an internal Label. This would allow you to remove the use of graphicListener and weakGraphicListener. If you want to display the graphic of the TreeItem as well then you would also remove cBox.setGraphic(null).
I make no attempt to cache the HBox (or the possible Label if you change things based on Note #2). Instead, I just create a new HBox every time. It should be simple to change the code so it does cache the HBox, however, if you so choose.
If you don't want to use CheckBoxTreeItem but would rather externalize whether or not an item is selected, you should look at these methods:
CheckBoxTreeCell.forTreeView(Callback)
CheckBoxTreeCell.forTreeView(Callback,StringConverter)
If you still want the graphic on the left of the CheckBox you'll have to extend CheckBoxTreeCell here as well. The only real difference to how CustomCheckBoxTreeCell is implemented above is you'd have to provide a way to set the Callback you would have used with the above two methods to the CustomCheckBoxTreeCell. You can do this by exposing a constructor that takes the Callback or by setting the selectedStateCallback property inside the cell factory.

JavaFX TreeView -- change background CSS on sibling nodes on MouseEvent

I am trying to implement onMouseEnter and onMouseExit events on a JavaFX ListView. What I want to do is if the mouse moves over a node of the list view, I want to change the background color of the nodes that are currently visible children in the current view.
This post has a great code sample, but is not quite what I am looking for.
Apply style to TreeView children nodes in javaFX
Using that code as a reference, what I am looking for is a given tree:
Root -> Item: 1 -> Item: 100 -> Item 1000, Item 1001, Item 1002, Item 1003
When I mouse over "Item: 100" I would like it and Item 1000* to have a background color change.
This seems difficult to me because the getNextSibling and getPreviousSibling interface is on the TreeItem and though you can get a TreeItem from a TreeCell on the MouseEvent, you can't (that I know of) update CSS on a TreeItem and have it take effect in a TreeCell -- because the setStyle method is on the TreeCell.
Suggestions on how this can be done?
[Update note: I originally had a solution using a subclass of TreeItem. The solution presented here is much cleaner than the original.]
Create an ObservableSet<TreeItem<?>> containing the TreeItems that should be highlighted. Then in the cell factory, observe that set, and the cell's treeItemProperty(), and set the style class (I used a PseudoClass in the example below) so the cell is highlighted if the tree item belonging to the cell is in the set.
Finally, register mouseEntered and mouseExited handlers with the cell. When the mouse enters the cell, you can get the tree item, use it to navigate to any other tree items you need, and add the appropriate items to the set you defined. In the mouseExited handler, clear the set (or perform other logic as needed).
import java.util.HashSet;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
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 HighlightingTree extends Application {
private final PseudoClass highlighted = PseudoClass.getPseudoClass("highlighted");
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setRoot(buildTreeRoot());
ObservableSet<TreeItem<Integer>> highlightedItems = FXCollections.observableSet(new HashSet<>());
tree.setCellFactory(tv -> {
// the cell:
TreeCell<Integer> cell = new TreeCell<Integer>() {
// indicates whether the cell should be highlighted:
private BooleanBinding highlightCell = Bindings.createBooleanBinding(() ->
getTreeItem() != null && highlightedItems.contains(getTreeItem()),
treeItemProperty(), highlightedItems);
// listener for the binding above
// note this has to be scoped to persist alongside the cell, as the binding
// will use weak listeners, and we need to avoid the listener getting gc'd:
private ChangeListener<Boolean> listener = (obs, wasHighlighted, isHighlighted) ->
pseudoClassStateChanged(highlighted, isHighlighted);
// anonymous constructor: register listener with binding
{
highlightCell.addListener(listener);
}
};
// display correct text:
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
cell.setText(null);
} else {
cell.setText(newItem.toString());
}
});
// mouse listeners:
cell.setOnMouseEntered(e -> {
if (cell.getTreeItem() != null) {
highlightedItems.add(cell.getTreeItem());
highlightedItems.addAll(cell.getTreeItem().getChildren());
}
});
cell.setOnMouseExited(e -> highlightedItems.clear());
return cell ;
});
BorderPane uiRoot = new BorderPane(tree);
Scene scene = new Scene(uiRoot, 600, 600);
scene.getStylesheets().add("highlight-tree-children.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Integer> buildTreeRoot() {
return buildTreeItem(1);
}
private TreeItem<Integer> buildTreeItem(int n) {
TreeItem<Integer> item = new TreeItem<>(n);
if (n < 10_000) {
for (int i = 0; i<10; i++) {
item.getChildren().add(buildTreeItem(n * 10 + i));
}
}
return item ;
}
public static void main(String[] args) {
launch(args);
}
}
highlight-tree-children.css:
.tree-cell:highlighted {
-fx-background: yellow ;
}

TextArea loose focus when using scrollBar

When you create a TextArea, you can listen to its "focusedProperty".
But if the user touch the inner scrollBar of the TextArea (if it's too small), the focus of the TextArea is lost (since the scrollBar has the focus).
But as far as I am concerned, the TextArea is still having the focus because the scrollBar are part or the TextArea and there's even no way of accessing them.
How can I hack the textArea so that I would detect when the user is using the scrollBar? I want to hack/create a focusedProperty that will return true when the user is typing text or using the scrollBar.
Observe the Scene's focusOwner property, and create a BooleanBinding that is true if it is a descendant of the text area and false otherwise:
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TextAreaFocusTest extends Application {
#Override
public void start(Stage primaryStage) {
TextArea textArea = new TextArea();
IntStream.rangeClosed(1, 200).forEach(i -> textArea.appendText(" "));
IntStream.rangeClosed(1, 80).forEach(i -> textArea.appendText("\nLine "+i));
Label label = new Label();
TextField textField = new TextField();
VBox root = new VBox(10, textArea, textField, label);
Scene scene = new Scene(root, 400, 400);
BooleanBinding focus = Bindings.createBooleanBinding(() -> {
for (Node n = scene.getFocusOwner(); n!= null ; n=n.getParent()) {
if (n == textArea) return true ;
}
return false ;
}, scene.focusOwnerProperty());
label.textProperty().bind(Bindings.when(focus).then("Focused").otherwise("Not Focused"));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Here is a variation on #James_D's answer, in case you need to be able to obtain the focus binding from his answer without having a reference to the scene, e.g. if you need to set up the bindings before the text area is added to the scene, are implementing a library, or just want to have your code less entangled.
This solution uses the EasyBind library for convenient selection of nested property (selecting focusOwnerProperty from the sceneProperty).
public static Binding<Boolean> containsFocus(Node node) {
return EasyBind.monadic(node.sceneProperty())
.flatMap(Scene::focusOwnerProperty)
.map(owner -> {
for (Node n = owner; n != null; n = n.getParent()) {
if (n == node) return true ;
}
return false ;
})
.orElse(false); // when node.getScene() is null
}

Resources