JavaFX font face dialog in Clojure - javafx

I'm building a Clojure program that includes an option dialog where the user can select the font used in an editor. Just like many other programs, I would like to present the user with a ComboBox where the dropdown list displays the font names in the font itself (e.g. 'Calibri' is displayed in the Calibri font, 'Arial' is displayed in Arial, and so on.)
In the past in Java, I have used a cell factory to customize the appearance of each cell in the list.
My translation into Clojure is not working though.
Here is what I have come up with so far:
(defn build-font-list-cell
"Return a Cell with an overridden updateItem implementation for
the cells in the font list combo. Format the name of the font in
the actual font."
[]
(proxy [TextFieldListCell] []
(updateItem [^String family mt]
(proxy-super updateItem family mt)
(if mt
(.setText this nil)
(do
(.setFont this (Font/font family))
(.setText this family))))))
(defn build-font-list-cell-factory
[]
(proxy [Callback] []
(call [list-view]
(build-font-list-cell))))
(defn build-font-face-combo
"Build, configure, and return the combo box used to select the font
face for the editor."
[]
(let [family-list (FXCollections/observableArrayList (Font/getFamilies))
font-face-combo (ComboBox. family-list)
current-face #tentative-font-face]
(.setEditable font-face-combo true)
(.addListener (.selectedItemProperty (.getSelectionModel font-face-combo))
^ChangeListener (face-combo-listener font-face-combo))
(.setCellFactory font-face-combo (build-font-list-cell-factory))
(select-item-in-combo font-face-combo current-face)
font-face-combo))
The compiler throws a ExceptionInInitializerError on this in the build-font-list-cell function at the declaration of the proxy. The IDE (IntelliJ) shows a warning about the updateItem argument in the call to 'super-proxy`, saying it cannot be resolved. I don't understand why not since it doesn't complain about the override on the line above.
This seems like a relatively straightforward translation of Java code that has worked before, but I'm clearly missing something. Or is this even the right approach to take?
EDIT: Adding the following MCVE. It compiles and runs as shown, but does not format the font face names of course. Attempting to create a cell factory by un-commenting the code in the listing produces something that the compiler chokes on.
(ns ffcbd.core
(:gen-class
:extends javafx.application.Application)
(:import (javafx.application Application)
(javafx.collections FXCollections)
(javafx.scene.control ComboBox)
(javafx.scene.control.cell TextFieldListCell)
(javafx.scene.text Font)
(javafx.scene.layout BorderPane)
(javafx.scene Scene)
(javafx.stage Stage)
(javafx.util Callback)))
;(defn build-font-list-cell []
; (proxy [TextFieldListCell] []
; (updateItem [family mt]
; (proxy-super updateItem family mt)
; (if mt
; (.setText this nil)
; (do
; (.setFont this (Font/font family))
; (.setText this family))))))
;(defn build-font-list-cell-factory []
; (proxy [Callback] []
; (call [list-view]
; (build-font-list-cell))))
(defn build-font-face-combo []
(let [family-list (FXCollections/observableArrayList (Font/getFamilies))
font-face-combo (ComboBox. family-list)]
; (.setCellFactory font-face-combo (build-font-list-cell-factory))
(.select (.getSelectionModel font-face-combo) 0)
font-face-combo))
(defn -start [this stage]
(let [root (BorderPane.)
scene (Scene. root)]
(.setTop root (build-font-face-combo))
(.add (.getChildren root) (build-font-face-combo))
(.setMinSize root 300 275)
(doto stage
(.setScene scene)
(.setTitle "Font Face ComboBox Demo")
(.show))))
(defn -main [& args]
(Application/launch ffcbd.core args))
Another difference from the Java version is that the list cell in Java is a ListCell. But I need to call super.updateItem. As I understand the docs, proxy does not allow you to call super unless the method is public. It is protected in ListCell, but public in TextFieldListCell.
EDIT #2: Here is an example of code that works in Java that I keep referring to.
package FontFaceDialog;
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.layout.BorderPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.scene.control.ComboBox;
import javafx.util.Callback;
public class Main extends Application {
private ComboBox<String> buildFontFaceCombo() {
ObservableList<String> lst = FXCollections.observableList(javafx.scene.text.Font.getFamilies());
ComboBox<String> cb = new ComboBox<String>(lst);
cb.getSelectionModel().select(0);
cb.setCellFactory((new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> listview) {
return new ListCell<String>() {
#Override
protected void updateItem(String family, boolean empty) {
super.updateItem(family, empty);
if (empty) {
setText(null);
} else {
setFont(Font.font(family));
setText(family);
}
}
};
}
}));
return cb;
}
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
root.setTop(buildFontFaceCombo());
primaryStage.setTitle("Font Face Dialog Example");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

There is no this in Clojure. I suspect that is the heart of the error.
The method signature is
public void updateItem(T item, boolean empty)
I would also suggest deleting the unnecessary ^String hint. If you haven't seen it yet, there are some examples on ClojureDocs.org

Unfortunately, I have been unable to figure out how to do this in Clojure so far. Since I need to get this working, I'm using another alternative. Since I know how to do it in Java, why not just do the mystery bits in Java? It's not as aesthetically pleasing, but it works.
Working with polyglot programs in lein is possible but kind of fiddly.
First, here is the Clojure part of the demo.
ns ffcbd.core
(:gen-class
:extends javafx.application.Application)
(:import (com.example FontFaceListCell)
(javafx.application Application)
(javafx.collections FXCollections)
(javafx.scene.control ComboBox)
(javafx.scene.text Font)
(javafx.scene.layout BorderPane)
(javafx.scene Scene)
(javafx.stage Stage)
(javafx.util Callback)))
(defn build-font-list-cell-factory []
(proxy [Callback] []
(call [list-view]
(FontFaceListCell.))))
(defn build-font-face-combo []
(let [family-list (FXCollections/observableArrayList (Font/getFamilies))
font-face-combo (ComboBox. family-list)]
(.setCellFactory font-face-combo (build-font-list-cell-factory))
(.select (.getSelectionModel font-face-combo) 0)
font-face-combo))
(defn -start [this stage]
(let [root (BorderPane.)
scene (Scene. root)]
(.setTop root (build-font-face-combo))
(.add (.getChildren root) (build-font-face-combo))
(.setMinSize root 400 275)
(doto stage
(.setScene scene)
(.setTitle "Font Face ComboBox Demo")
(.show))))
(defn -main [& args]
(Application/launch ffcbd.core args))
Notice the import of com.example.FontFaceListCell near the top. That's the Java part. Here's the listing of that little class.
package com.example;
import javafx.scene.control.ListCell;
import javafx.scene.text.Font;
public class FontFaceListCell extends ListCell<String> {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setFont(Font.font(item, 16.0d));
setText(item);
}
}
}
This class extends ListCell and overrides the updateItem method. When you run the program and click on the ComboBox, I get something like this on my system.
As mentioned above, to get this working with lein, you need a few more fiddly bits.
In order to compile Java code, lein needs to know where Java is. I added this line to my global profile.
:java-cmd "C:\\Program Files\\Java\\jdk1.8.0_121\\bin\\java.exe"
This kinda sucks because now I can't use the same profiles.clj file on Windows and Linux.
Getting this to work also requires a few changes to the project.clj to tell lein where the Java code is and which options to pass to the compiler. Here is what I used.
(defproject ffcbd "0.1.0-SNAPSHOT"
:description "A demo of a styled font selection ComboBox in Clojure."
:dependencies [[org.clojure/clojure "1.8.0"]]
:java-source-paths ["java"]
:javac-options ["-target" "1.8" "-source" "1.8"]
:aot :all
:main ffcbd.core)
I've put up a Mercurial repo on Bitbucket including an IntelliJ IDEA project for anyone interested.
It would still be nice to get all of this working only using Clojure though. I'm sure gen-class would do it, but haven't figured it out yet.

Related

JavaFX: ComboBox with custom cell factory - buggy rendering

I have a ComboBox with a custom cell factory, but the rendering is buggy and I don't know what I'm doing wrong.
The ComboBox's data is based on an enum, let's call it MyEnum. So my ComboBox is defined as:
#FXML
private ComboBox<MyEnum> comboBox;
I am setting it up like this:
comboBox.getItems().addAll(MyEnum.values());
comboBox.setButtonCell(new CustomRenderer());
comboBox.setCellFactory(cell -> new CustomRenderer());
comboBox.getSelectionModel().select(0);
My CustomRenderer looks like this:
public class CustomRenderer extends ListCell<MyEnum> {
#Override
protected void updateItem(MyEnum enumValue, boolean empty) {
super.updateItem(enumValue, empty);
if (enumValue== null || empty) {
setGraphic(null);
setText(null);
} else {
setGraphic(enumValue.getGraphic());
}
}
}
The getGraphic() method of MyEnum returns a HBox which can contain any number of elements, but in my specific case it's just an ImageView:
public enum MyEnum{
ONE(new ImageView(new Image(MyApp.class.getResourceAsStream("image1.png"),
15,
15,
true,
true))),
TWO(new ImageView(new Image(MyApp.class.getResourceAsStream("image2.png"),
15,
15,
true,
true)));
private final HBox graphic;
MyEnum(Node... graphicContents) {
graphic = new HBox(graphicContents);
graphic.setSpacing(5);
}
public HBox getGraphic() {
return graphic;
}
}
Now, when I start the app, the first bug is that the ComboBox doesn't show anything selected, even though I have the comboBox.getSelectionModel().select(0) in the initialization:
When I click on it, the dropdown is correct and shows my two entries with their images:
When I select one of the entries, everything still seems fine:
But when I open the dropdown again, then it looks like this:
So suddenly the selected image is gone from the dropdown.
After I select the other entry where the icon is still displayed, and reopen the dropdown, then both images are gone. The images are still shown in the ButtonCell though, just not in the dropdown.
I first thought maybe it has something to do specifically with ImageViews, but when I replaced them with other nodes, like Labels, it was still the same 2 bugs:
Nothing shown as selected on app start
Everything that I click in the dropdown box is then gone from the dropdown
If a runnable sample is needed, let me know. But maybe someone can already spot my mistake from the given code.
Thx
Your issue is that a node cannot appear in the scene graph more than once.
From the node documentation:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in all of the following: as the root node of a Scene, the children ObservableList of a Parent, or as the clip of a Node.
If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.
You are trying to reuse the same node in both the button for the list selection and in the selection list itself, which is not allowed.
Additionally, as noted in comments by kleopatra: "it's wrong to use nodes as data". One reason for that (among others), is that if you want to have more than one view of the same data visible at the same time, you won't be able to because the node related to the data can only be attached to the scene at a single place at any given time.
I'd especially recommend not placing nodes in enums. In my opinion, that it is really not what enums are for.
What you need to do is separate the model from the view. The cell factory is creating a view of the changing value of the enum which is the model. This view only needs to be created once for the cell and it needs to be updated (by providing the view with the appropriate image) whenever the cell value changes.
Example Code
You can see in the example that, because we have created two separate views of the same Monster (the dragon), the example is able to render both views at the same time. This is because the views are completely different nodes, that provide a view to the same underlying data (an enum value and its associated image).
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ChooseYourDoomApp extends Application {
public static final String CSS = "data:text/css," + // language=CSS
"""
.root {
-fx-background-color: lightblue;
-fx-base: palegreen;
}
""";
#Override
public void start(Stage stage) throws Exception {
ComboBox<Monster> choiceOfDoom = new ComboBox<>(
FXCollections.observableArrayList(
Monster.values()
)
);
choiceOfDoom.setButtonCell(new MonsterCell());
choiceOfDoom.setCellFactory(listView -> new MonsterCell());
choiceOfDoom.getSelectionModel().select(Monster.Dragon);
StackPane layout = new StackPane(choiceOfDoom);
layout.setPadding(new Insets(20));
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS);
stage.setScene(scene);
stage.show();
}
public class MonsterCell extends ListCell<Monster> {
private ImageView imageView = new ImageView();
#Override
protected void updateItem(Monster item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
imageView.setImage(null);
setGraphic(null);
} else {
imageView.setImage(monsterImages.get(item));
setGraphic(imageView);
}
}
}
public enum Monster {
Medusa,
Dragon,
Treant,
Unicorn
}
private Map<Monster, Image> monsterImages = createMonsterImages();
private Map<Monster, Image> createMonsterImages() {
Map<Monster, Image> monsterImages = new HashMap<>();
for (Monster monster : Monster.values()) {
monsterImages.put(
monster,
new Image(
Objects.requireNonNull(
ChooseYourDoomApp.class.getResource(
monster + "-icon.png"
)
).toExternalForm()
)
);
}
return monsterImages;
}
public static void main(String[] args) {
launch(args);
}
}
Icons are placed in the resource directory under the same hierarchy as the package containing the main application code.
https://iconarchive.com/show/role-playing-icons-by-chanut.html
Dragon-icon.png
Medusa-icon.png
Treant-icon.png
Unicorn-icon.png

Verify inner/top nodes inside Parent node

I have an AnchorPane which has a considerable amount of containers inside of it (most of them are AnchorPanes as well): MainAnchorPane is always set as the top anchorPane and it has multiple anchorPane as children(let's say these are AnchorPane2 and AnchorPane3).
Looking forward to finding a way to identify which anchorPane is inside of the main container, I've tried to do the following:
if (mainAnchorPane.getChildren().contains(anchorPane2))
{ System.out.println("anchorPane2 is opened here"); }
else if (mainAnchorPane.getChildren().contains(anchorPane3))
{System.out.println("anchorPane3 is opened here");}
else {System.out.println("no anchorPane found");}
Although, this method does not work.
I also tried to compare anchorPanes from another classes, but that gives me NullPointerException (even with that anchorPane being open is another part of the system -For instance, I have multiple MainAnchorPanes which can handle the same anchorPane2 and 3 to be open at the same time-):
if (mainAnchorPane.getChildren().contains(anotherClass.anchorPane2))
{System.out.println("anchorPane2 is opened here");}
else {System.out.println("no anchorPane found");}
That would be my preferred method, but as I mentioned, for some reason it gives me a null exception in the life of the if statement.
Finally, I have tried the last effort to identify which anchorPane is currently being shown in my MainAnchorPane by creating a list of nodes inside the mainAnchorPane and comparing each of them with the target node, which is anchorPane2 or anchorPane3:
for (Node node: getAllNodes(mainAnchorPane))
{
System.out.println(node);
// in the place of equals() I've tried contains() as well
if (node.equals(anchorPane2)) {
System.out.println("ap2 is here");
}
else {System.out.println("ap2 is not here");}
}
public static ArrayList<Node> nodes = new ArrayList<Node>();
public static ArrayList<Node> getAllNodes(Parent root) {
addAllDescendents(root, nodes);
return nodes;
}
private static void addAllDescendents(Parent parent, ArrayList<Node> nodes) {
for (Node node : parent.getChildrenUnmodifiable()) {
nodes.add(node);
if (node instanceof Parent)
addAllDescendents((Parent)node, nodes);
}
}
None of these methods worked. I even went further and checked whether my ifs catch buttons and labels nodes inside of AnchorPanes which are nodes of that pane, although the condition always returns as false, suggesting the node is not in there (which is not true, since if shows up in all in System.out.println(nodes, mainAnchorPane.getChildren(), and getAllNodes(mainAnchorPane)))
Any thoughts would be appreciated.
Edit: it should be as simple as
if (mainAnchorPane.getChildren().contains(node))
but that doesn't work as well
Naming convention were simplified aiming for better explanation.
On high level your code looks fine. Having said that there are some considerable changes/cleanup you need to do.
no need of static ArrayList
no need to check in a for loop.
...
I quickly tried your code with some changes and it is working fine. Try modifying accordingly. Even if you have issues.. then you might be doing something wrong elsewhere.
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
public class LayoutDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
AnchorPane anchorPane2 = new AnchorPane(new StackPane());
AnchorPane anchorPane3 = new AnchorPane(new StackPane());
// Change the internal node to anchorPane3 to get "not here" output
AnchorPane mainAnchorPane = new AnchorPane(new StackPane(new Pane(new VBox(anchorPane2, new Pane()), new Pane())), new Pane());
Pane root = new Pane(mainAnchorPane);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
if (getAllNodes(mainAnchorPane).contains(anchorPane2)) {
System.out.println("ap2 is here");
} else {
System.out.println("ap2 is not here");
}
}
public static List<Node> getAllNodes(Parent root) {
List<Node> nodes = new ArrayList<Node>();
addAllDescendents(root, nodes);
return nodes;
}
private static void addAllDescendents(Parent parent, List<Node> nodes) {
for (Node node : parent.getChildrenUnmodifiable()) {
nodes.add(node);
if (node instanceof Parent)
addAllDescendents((Parent) node, nodes);
}
}
}

return type of getStyleableProperty

When you look docs about what returns a given CSSMetadata (getCSSmetadata) the function getStyleableProperty tells something about <? capture of extends styleable
what is the type and how does it work.
I try to cast -fx-max-width to ( easyno subproperties) Styleable but does not work
hbox.getCSSMetadata().stream().filter(prop -> prop.toString().constanis("-fx-max-width")).forEach(prop2->{
prop2.getProperty(); //ok returns the string of the name
prop2.getStyleableProperty(<????? what goes here and what is the type of the returned value>);
});
Your prop2 variable (which is not really well named) is of type CSSMetaData<? extends Styleable, ?>.
The parameter you need to pass to the getStyleableProperty(...) method is the Styleable for which it's a property; since this CSSMetaData came from hbox, and I assume that's an HBox, then the parameter should be hbox.
However, the compiler will insist the parameter is the same type as the first type parameter to the CSSMetaData; since this is a wildcard type (? extends Styleable) there's no way for it to check this. So you need a downcast:
((CSSMetaData<HBox, ?>)prop2).getStyleableProperty(hbox)
The cast does not need to be this specific;
((CSSMetaData<Styleable, ?>)prop2).getStyleableProperty(hbox)
will also work (since hbox is an HBox, which is an implementation of Styleable).
Note that this is actually going to give you the hbox's maxWidthProperty(), so really you could just do hbox.maxWidthProperty() instead. (But maybe your -fx-max-width is just a contrived example, and you are trying to get this dynamically for some reason.)
Note that it's almost always bad practice to check an object's toString() method to determine data about it. So you should replace
prop.toString().contains("-fx-max-width")
with
prop.getProperty().equals("-fx-max-width)
The return type of getStyleableProperty(), with this downcast, will be
StyleableProperty<?>
(since there is a wildcard for the second type parameter in the downcast). If you knew more information, as in this case, you can make that more specific if needed. For example, if you wanted to set the value, you would need to use the fact that the -fx-max-width CSS property is numeric, and use the downcast
StyleableProperty<Number> maxWidth = ((CSSMetaData<Styleable, Number>)prop2).getStyleableProperty(hbox);
and then the return type would be
StyleableProperty<Number>
and you'd be able to do, for example
maxWidth.setValue(400.0);
Here's an example:
import javafx.application.Application;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class SPTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setAlignment(Pos.CENTER);
Button show = new Button("Show");
show.setOnAction(e -> {
root.getCssMetaData().stream().filter(cssMD -> cssMD.getProperty().equals("-fx-max-width")).forEach(maxWidthMD -> {
StyleableProperty<?> maxWidth = ((CssMetaData<Styleable, ?>)maxWidthMD).getStyleableProperty(root);
System.out.println(maxWidth.getValue());
System.out.println(maxWidth == root.maxWidthProperty());
});
System.out.println(root.getMaxWidth());
});
root.getChildren().add(show);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
I used a simple style.css file to test this:
.root {
-fx-max-width: 400 ;
}
Note the compiler has no way of checking the cast will work, so you get a compiler warning with this code. Since you're assured the cast will work (because the CSSMetaData was retrieved from the hbox), you can suppress this warning:
#SuppressWarnings("unchecked")
StyleableProperty<?> maxWidth = ((CssMetaData<HBox, ?>)prop2).getStyleableProperty(root);

Handle the ^ and ê key press with JavaFX

From a JavaFX application, I would like to be able to type letters like ê, ô, etc. Using linux and my keyboard mapping, it's done with dead chars (ie. typing ^, then e).
I also tried to handle the ^ key directly. According to the Javadoc, the key code for ^ is KeyCode.CIRCUMFLEX.
Here is a simple key press test application, which print the name (e.getCode().getName()) and and the text (e.getText()) of a key each time a key is pressed:
package sample;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane pane = new BorderPane();
Label label = new Label("Type some text...");
pane.setLeft(label);
Scene scene = new Scene(pane, 200, 40);
primaryStage.setScene(scene);
primaryStage.show();
scene.setOnKeyPressed(e -> label.setText("name: " + e.getCode().getName() + "\ntext: " + e.getText()));
}
public static void main(String[] args) {
launch(args);
}
}
When I run this application:
I can't display character requiring dead letters (like ê);
The code for the key ^ is UNDEFINED, so I can't handle this key with something like if(e.getCode() == KeyCode.CIRCUMFLEX).
I am wrongly using key events or is it a JavaFX issue? I use Ubuntu 16.04 and Java8.
Edit: A screenshot:
One line with KeyEvent(using KeyTyped Event)💢
[Works both for the keyboard i use which needs Shift+6 to print ^]:
scene.addEventFilter(KeyEvent.KEY_TYPED,
event -> label.setText("name: " + event.getCharacter() + "\ntext: " + event.getCharacter()));
So to detect if ^ is typed you can use inside the event:
if(event.getCharacter().equals("^"){
.....
}

JavaFX TreeView buggy behaviour

I created a TreeView with checkbox tree items. I wanted to add listener to each checkbox, so I created a sample problem. But I got some pretty weird behavior. When I click on a checkbox, it actually triggered the handler for both itself and the one above it.
Here's the code I wrote:
package sample;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.layout.AnchorPane;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable{
#FXML
public AnchorPane container;
public static class CustomTreeCell extends CheckBoxTreeCell<String> {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
((CheckBox) getGraphic()).selectedProperty().addListener(((observable, oldValue, newValue) -> {
System.out.println("Clicked on: " + item);
}));
}
}
}
#Override
public void initialize(URL location, ResourceBundle resources) {
TreeView<String> treeView = new TreeView<>();
treeView.setCellFactory((TreeView<String> param) -> new CustomTreeCell());
container.getChildren().add(treeView);
CheckBoxTreeItem root = new CheckBoxTreeItem<>("Root");
treeView.setRoot(root);
CheckBoxTreeItem leaf_A = new CheckBoxTreeItem<>("Leaf A");
CheckBoxTreeItem leaf_B = new CheckBoxTreeItem<>("Leaf B");
CheckBoxTreeItem leaf_C = new CheckBoxTreeItem<>("Leaf C");
root.getChildren().addAll(leaf_A, leaf_B, leaf_C);
}
}
After running the application, when I click on Leaf B, it prints out this:
Clicked on: Leaf A
Clicked on: Leaf B
I have been trying to figure out why this is happening for days but still couldn't find out why.
Any hints are appreciated!
Thanks!
updateItem is called any time the cell is reused to show a different item. This can happen quite often, and the API (deliberately) does not specify exactly when (i.e. it's an implementation detail). Typically, though, it happens frequently if, for example, the user scrolls in the table.
Your updateItem() method adds new listeners to the check box every time, and never removes those listeners. So what you might observe is more and more listeners getting fired the more you have scrolled in the table. (Try adding enough items to your table to allow scrolling, scroll around a lot, and then check/uncheck a check box.)
The intended use is to set the selectedStateCallBack to map to a property in your model class. Then if you want to react to changes, you can do so by observing the property in the model. This is explained in CheckBoxTableCell changelistener not working.

Resources