How to traverse the entire scene graph hierarchy? - javafx

I would like to write a method that will print the entire scene graph (including the names of the nodes and the css properties associated with the nodes) to the console. This would enable viewing the hierarchy of the scene graph and all the css attributes associated with each element. I tried to do this (see the scala code below) but my attempt failed.
The scene graph I'm testing this on contains a custom controller that I made following this tutorial. It is not printing any of the nested controls contained in the custom controller. The custom controller appears fine on the stage (and it functions properly) so I know all the required controls are part of the scene graph, but for some reason the example code does not recurse into the custom controller. It does print the name of the custom controller, but not the nested elements inside the controller.
object JavaFXUtils {
def printNodeHierarchy(node: Node): Unit = {
val builder = new StringBuilder()
traverse(0, node, builder)
println(builder)
}
private def traverse(depth: Int, node: Node, builder: StringBuilder) {
val tab = getTab(depth)
builder.append(tab)
startTag(node, builder)
middleTag(depth, node, builder)
node match {
case parent: Parent => builder.append(tab)
case _ =>
}
endTag(node, builder)
}
private def getTab(depth: Int): String = {
val builder = new StringBuilder("\n")
for(i <- 0 until depth) {
builder.append(" ")
}
builder.toString()
}
private def startTag(node: Node, builder: StringBuilder): Unit = {
def styles: String = {
val styleClasses = node.getStyleClass
if(styleClasses.isEmpty) {
""
} else {
val b = new StringBuilder(" styleClass=\"")
for(i <- 0 until styleClasses.size()) {
if(i > 0) {
b.append(" ")
}
b.append(".").append(styleClasses.get(i))
}
b.append("\"")
b.toString()
}
}
def id: String = {
val nodeId = node.getId
if(nodeId == null || nodeId.isEmpty) {
""
} else {
val b = new StringBuilder(" id=\"").append(nodeId).append("\"")
b.toString()
}
}
builder.append(s"<${node.getClass.getSimpleName}$id$styles>")
}
private def middleTag(depth: Int, node: Node, builder: StringBuilder): Unit = {
node match {
case parent: Parent =>
val children: ObservableList[Node] = parent.getChildrenUnmodifiable
for (i <- 0 until children.size()) {
traverse(depth + 1, children.get(i), builder)
}
case _ =>
}
}
private def endTag(node: Node, builder: StringBuilder) {
builder.append(s"</${node.getClass.getSimpleName}>")
}
}
What is the proper way to print the contents of the entire scene graph? The accepted answer can be written in either Java or Scala.
Update
Upon further review, I'm noticing that it does work properly for some of the custom controls, but not all of them. I have 3 custom controls. I'll demonstrate 2 of them. The custom controls are ClientLogo and MenuViewController. The previously listed traversal code properly shows the children of the ClientLogo, but does not show the children of the MenuViewController. (Maybe this is because the MenuViewController is a subclass of TitledPane?)
client_logo.fxml:
<?import javafx.scene.image.ImageView?>
<fx:root type="javafx.scene.layout.StackPane" xmlns:fx="http://javafx.com/fxml" id="clientLogo">
<ImageView fx:id="logo"/>
</fx:root>
ClientLogo.scala
class ClientLogo extends StackPane {
#FXML #BeanProperty var logo: ImageView = _
val logoFxml: URL = classOf[ClientLogo].getResource("/fxml/client_logo.fxml")
val loader: FXMLLoader = new FXMLLoader(logoFxml)
loader.setRoot(this)
loader.setController(this)
loader.load()
logo.setImage(Config.clientLogo)
logo.setPreserveRatio(false)
var logoWidth: Double = .0
def getLogoWidth = logoWidth
def setLogoWidth(logoWidth: Double) {
this.logoWidth = logoWidth
logo.setFitWidth(logoWidth)
}
var logoHeight: Double = .0
def getLogoHeight = logoHeight
def setLogoHeight(logoHeight: Double) {
this.logoHeight = logoHeight
logo.setFitHeight(logoHeight)
}
}
Usage of ClientLogo:
<ClientLogo MigPane.cc="id clientLogo, pos (50px) (-25px)"/>
menu.fxml:
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.control.Label?>
<fx:root type="javafx.scene.control.TitledPane" xmlns:fx="http://javafx.com/fxml" id="menu" collapsible="true" expanded="false">
<GridPane vgap="0" hgap="0">
<children>
<Line fx:id="line" styleClass="menu-line" startX="0" startY="1" endX="150" endY="1" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<Label fx:id="btnSettings" id="btn-settings" styleClass="btn-menu" text="%text.change.password" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<Label fx:id="btnAdmin" id="btn-admin" styleClass="btn-menu" text="%text.admin" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<Label fx:id="btnQuickTips" id="btn-quick-tips" styleClass="btn-menu" text="%text.quick.tips" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<Label fx:id="btnLogout" id="btn-logout" styleClass="btn-menu" text="%text.logout" GridPane.rowIndex="4" GridPane.columnIndex="0"/>
</children>
</GridPane>
</fx:root>
MenuViewController.scala:
class MenuViewController extends TitledPane with ViewController[UserInfo] with LogHelper with Resources {
private var menuWidth: Double = .0
#FXML #BeanProperty var line: Line = _
#FXML #BeanProperty var btnSettings: Label = _
#FXML #BeanProperty var btnAdmin: Label = _
#FXML #BeanProperty var btnQuickTips: Label = _
#FXML #BeanProperty var btnLogout: Label = _
val menuFxml: URL = classOf[MenuViewController].getResource("/fxml/menu.fxml")
val loader: FXMLLoader = new FXMLLoader(menuFxml)
loader.setRoot(this)
loader.setController(this)
loader.setResources(resources)
loader.load()
var userInfo: UserInfo = _
#FXML
private def initialize() {
def handle(message: Any): Unit = {
setExpanded(false)
uiController(message)
}
btnSettings.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(SettingsClicked)))
btnAdmin.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(AdminClicked)))
btnQuickTips.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(QuickTipsClicked)))
btnLogout.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(LogoutClicked)))
}
override def update(model: UserInfo) {
userInfo = model
setText(if (userInfo == null) "Menu" else userInfo.displayName)
}
def getMenuWidth: Double = {
return menuWidth
}
def setMenuWidth(menuWidth: Double) {
this.menuWidth = menuWidth
val spaceToRemove: Double = (menuWidth / 3)
line.setEndX(menuWidth - spaceToRemove)
line.setTranslateX(spaceToRemove / 2)
Array(btnSettings, btnAdmin, btnQuickTips, btnLogout).foreach(btn => {
btn.setMinWidth(menuWidth)
btn.setPrefWidth(menuWidth)
btn.setMaxWidth(menuWidth)
})
}
}
Usage of MenuViewController:
<MenuViewController MigPane.cc="id menu, pos (100% - 250px) (29)" menuWidth="200" fx:id="menu"/>
This is a sample of the output from printing the scene graph to the console:
<BorderPane styleClass=".root">
<StackPane>
<MigPane id="home" styleClass=".top-blue-bar-bottom-blue-curve">
<ClientLogo id="clientLogo">
<ImageView id="logo"></ImageView>
</ClientLogo>
...
<MenuViewController id="menu" styleClass=".titled-pane">
</MenuViewController>
</MigPane>
</StackPane>
</BorderPane>
As you can see, the child elements in the ClientLogo custom control are properly traversed, but the elements inside menu.fxml are missing. I would expect to see the substructure that is added to a TitledPane and the GridPane with its nested substructure listed in menu.fxml.

Suggested Tool
ScenicView is a 3rd party tool for introspecting on the SceneGraph. It is highly recommended that you use ScenicView rather than developing your own debugging tools to dump the Scene Graph.
Dump Utility
Here is a very simple little routine to dump the scene graph to System.out, invoke it via DebugUtil.dump(stage.getScene().getRoot()):
import javafx.scene.Node;
import javafx.scene.Parent;
public class DebugUtil {
/** Debugging routine to dump the scene graph. */
public static void dump(Node n) {
dump(n, 0);
}
private static void dump(Node n, int depth) {
for (int i = 0; i < depth; i++) System.out.print(" ");
System.out.println(n);
if (n instanceof Parent) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
dump(c, depth + 1);
}
}
}
}
The simple routine above will usually dump all of the style class info because the default toString() on node outputs styleclass data.
An additional enhancement would be to check if the node being printed is also an instance of Labeled or Text, then also print the text string associated with the node in those cases.
Caveat
For some control types, some of the nodes which are created for the control are instantiated by the control's skin which can be defined in CSS and the skin sometimes creates its child nodes lazily.
What you need to do to see those nodes is ensure a css layout pass has been run on the scene graph by running your dump after one of the following events:
stage.show() has been called for an initial scene.
After a pulse occurs for a modified scene.
use AnimationTimer for this to count a pulse.
After you have manually forced a layout pass.
taking a synchronous snapshot of the scene will force a layout pass, though I think there may be some other APIs that I can't remember the name of which allow you to force the layout pass without a snapshot.
Check that you have done one of these things before you run your dump.
Example of using an AnimationTimer
// Modify scene.
// . . .
// Start a timer which delays the scene dump call.
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
// Take action on receiving a pulse.
dump(stage.getScene().getRoot());
// Stop monitoring pulses.
// You can stop immediately like this sample.
stop();
// OR you could count a specified number pulses before stopping.
}
};
timer.start();

Based on #jewelsea's excellent answer , heres a version in Jython. It traverses a bit deeper by using a special case handler for controls that dont respond well to the findchildren method. It should be straight forward enough to translate into generic java, just use the python code as pseudocode!
from javafx.scene import Parent
from javafx.scene import control
class SceneGraph(object):
def parse(self,n,depth=0):
print " "*depth,n
if isinstance(n,control.Tab):
for i in n.getContent().getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.TabPane):
for i in n.getTabs():
self.parse(i,depth+1)
elif isinstance(n,Parent):
for i in n.getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.Accordion):
for i in n.getPanes():
self.parse(i,depth+1)
Special handlers might be needed for other controls, and the code could perhaps be improved by a closer examination of the class heirachy.
I'm not entirely sure how best to handle third party control packs, at this stage.

Related

JavaFX ListChangeListener handles 'removeAll(Collection)' inconsistently based on position of removed items in ObservableList

I've encountered what appears to be an anomaly in how ListChangeListener handles batch removals (i.e. removeAll(Collection). If the items in the Collection are contiguous then the handling operation specified in the listener works fine. However, if the Collection are not contiguous then the operation specified in the listener halts once contiguity is broken.
This can best be explained by way of example. Assume the ObservableList consists of the following items:
"red"
"orange"
"yellow"
"green"
"blue"
Assume also that there is a separate ObservableList that tracks the hashCode values for the colors, and that a ListChangeListener has been added that removes the hashCode from the second list whenever one or more of the items in the first list is removed. If the 'removal' Collection consists of "red", "orange" and "yellow" then the code in the listener removes the hashCodes for all three items from the second list as expected. However, if the 'removal' Collection consists of "red", "orange" and "green", then the code in the listener stops after removing the hashCode for "orange" and never reaches "green" like it should.
A short app that illustrates the problem is set out below. The listener code is in a method named buildListChangeListener() that returns a listener that is added to the 'Colors' list. To run the app it helps to know that:
'consecutive' in the ComboBox specifies three colors that are contiguous as explained above; clicking the 'Remove' button will cause them to be removed from the 'Colors' list and their hashCodes
from the other list.
'broken' specifies three colors that are not contiguous, so that
clicking the 'Remove' button removes only one of the colors
clicking 'Refresh' restores both lists to their original state
Here's the code for the app:
package test;
import static java.util.Objects.isNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Pair;
public class RemoveAllItemsBug extends Application {
private StackPane stackPane;
private HBox hbox;
private VBox vbox1;
private Label label1;
private ListView<Pair<String, Color>> colors;
private VBox vbox2;
private Label label2;
private ListView<Integer> hashCodes;
private VBox vbox3;
private Label label3;
private ComboBox<String> actionModes;
private Button btnRemove;
private Button btnRefresh;
final static private String CONSECUTIVE = "consecutive", BROKEN = "broken";
private final EventHandler<WindowEvent> onCloseRequestListener = evt -> {
Platform.exit();
System.exit(0);
};
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("DUMMY APP");
// Necessary to ensure stage closes completely and javaw.exe stops running
primaryStage.setOnCloseRequest(onCloseRequestListener);
primaryStage.setWidth(550);
primaryStage.setHeight(310);
// primaryStage.setMinWidth(550);
// primaryStage.setMinHeight(310);
/*
* Code block below for width/height property printouts is used to
* test for an optimal size for the app. Once the size is determined
* they may (and should be) commented out as here.
*/
primaryStage
.widthProperty()
.addListener((width, oldWidth, newWidth) -> {
System.out.println("width: " + newWidth);
});
primaryStage
.heightProperty()
.addListener((height, oldHeight, newHeight) -> {
System.out.println("height: " + newHeight);
});
initializeUI();
installSimpleBehavior();
installListChangeListener();
primaryStage.setScene(new Scene(stackPane));
primaryStage.show();
}
private void installListChangeListener() {
/*
* The 'listChangeListenerUsingIf()' method returns a listener that
* uses an 'if (c.next()) ...' statement to access the first change in
* the Change variable (c). For purposes of accessing the first change
* this is functionally equivalent to a 'while (c.next()) ...'
* statement. However, because the Change variable may contain
* multiple 'remove' changes where each change is represented by a
* separate 'getRemoved()' list, the 'if (c.next())' statement will
* catch only the first change while the 'while (c.next())' statement
* (which is used in the 'listChangeListenerUsingWhile()' method)
* catches them all.
*
* The code below should be commented out as appropriate before
* running the app in order to see the difference.
*
* This case illustrates a serious flaw in the ListChangeListener API
* documentation because it fails to indicate that the Change variable
* may include multiple 'remove' changes and that each such change
* must be accessed in a separate iteration (e.g. the 'while
* (c.next()...').
*
* In contrast, 'add' changes (i.e. changes resulting from the
* addition of one or more items to the source list), the name of the
* method that returns the change(s) is 'getAddSublist()'. This
* clearly indicates that there may be more than one list of items
* that have been added, or similarly that the total items that have
* been 'added' by the change(s) represented by the Change variable
* may be included in more than one list; thus the use of the term
* 'sublist'.
*
* The flaw is illustrated further in the cautionary note in the API
* that reads as follows:
*
* "[I]n case the change contains multiple changes of different type,
* these changes must be in the following order: <em> permutation
* change(s), add or remove changes, update changes </em> This is
* because permutation changes cannot go after add/remove changes as
* they would change the position of added elements. And on the other
* hand, update changes must go after add/remove changes because they
* refer with their indexes to the current state of the list, which
* means with all add/remove changes applied."
*
* This is certainly useful information. However, the problems
* illustrated by the case at hand (i.e. different treatment based on
* whether the changed items are continguous in the source list) are
* just as significant as the situation addressed by the note, yet
* they are not mentioned.
*
* A better understanding as to how the process works can be gained by
* running a system printout for the Change variable class
* (System.out.println("Change variable class: " +
* c.getClass().getSimpleName())) and compare the results yielded from
* changing the choice in the 'Action modes' combo box from
* 'consecutive' to 'broken'. For 'consecutive' (i.e. continguous),
* the class for the Change variable is
* ListChangeBuilder$SingleChange, for 'broken' (i.e. non-continguous)
* the class is ListChangeBuilder$IterableChange. These classes aren't
* well documented, which while regrettable is understandable inasmuch
* as they're private inner classes for restricted API. Interestingly,
* however, there is a public class MultipleAdditionAndRemovedChange
* (also restricted API) that appears to fit this case perfectly and
* is a bit more informative.
*/
// colors.getItems().addListener(listChangeListenerUsingIf());
colors.getItems().addListener(listChangeListenerUsingWhile());
}
private void initializeUI() {
//- Controls for colors
label1 = new Label("Colors");
colors = new ListView<Pair<String, Color>>();
colors.setPrefSize(150, 200);
colors.setItems(FXCollections.observableList(new ArrayList<>(colorsList())));
vbox1 = new VBox(label1, colors);
//- Controls for colors
label2 = new Label("Hash codes");
hashCodes = new ListView<Integer>();
hashCodes.setPrefSize(150, 200);
hashCodes.setItems(FXCollections.observableList(new ArrayList<>(
colorsList().stream()
.map(e -> e.hashCode())
.collect(Collectors.toCollection(ArrayList::new)))));
vbox2 = new VBox(label2, hashCodes);
//- 'Action mode' controls
label3 = new Label("Action mode");
actionModes = new ComboBox<>(
FXCollections.observableList(List.of(CONSECUTIVE, BROKEN)));
actionModes.setPrefWidth(150);
actionModes.getSelectionModel().select(0);
btnRemove = new Button("Remove");
btnRefresh = new Button("Refresh");
List.of(btnRemove, btnRefresh).forEach(b -> {
b.setMaxWidth(Double.MAX_VALUE);
VBox.setMargin(b, new Insets(5, 0, 0, 0));
});
vbox3 = new VBox(label3, actionModes, btnRemove, btnRefresh);
hbox = new HBox(vbox1, vbox2, vbox3);
hbox.setPadding(new Insets(10));
hbox.setSpacing(15);
hbox.setBackground(new Background(
new BackgroundFill(Color.DARKGRAY, CornerRadii.EMPTY, Insets.EMPTY),
new BackgroundFill(Color.WHITESMOKE, CornerRadii.EMPTY, new Insets(1))));
stackPane = new StackPane(hbox);
stackPane.setPadding(new Insets(15));
}
private void installSimpleBehavior() {
//- 'Colors' cell factory
colors.setCellFactory(listView -> {
return new ListCell<Pair<String, Color>>() {
#Override
protected void updateItem(Pair<String, Color> item, boolean empty) {
super.updateItem(item, empty);
if (isNull(item) || empty) {
setGraphic(null);
setText(null);
}
else {
HBox graphic = new HBox();
graphic.setPrefSize(15, 15);
graphic.setBackground(new Background(new BackgroundFill(
item.getValue(),
CornerRadii.EMPTY,
Insets.EMPTY)));
setGraphic(graphic);
setText(item.getKey());
setContentDisplay(ContentDisplay.LEFT);
}
}
};
});
//- 'Colors' cell factory
hashCodes.setCellFactory(listView -> {
return new ListCell<Integer>() {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (isNull(item) || empty) {
setGraphic(null);
setText(null);
}
else {
HBox graphic = new HBox();
graphic.setPrefSize(15, 15);
graphic.setBackground(new Background(new BackgroundFill(
colorForHashCode(item),
CornerRadii.EMPTY,
Insets.EMPTY)));
Canvas c = new Canvas(15, 15);
GraphicsContext graphics = c.getGraphicsContext2D();
graphics.setFill(colorForHashCode(item));
graphics.fillRect(0, 0, c.getWidth(), c.getHeight());
setGraphic(c);
setText("" + item);
setContentDisplay(ContentDisplay.LEFT);
}
}
private Color colorForHashCode(int hash) {
return colorsList().stream()
.filter(e -> e.hashCode() == hash)
.map(e -> e.getValue())
.findFirst()
.orElseThrow();
}
};
});
//- 'Remove' button action
btnRemove.setOnAction(e -> {
String actionMode = actionModes.getValue();
if (CONSECUTIVE.equals(actionMode)) {
colors.getItems().removeAll(consecutiveColors());
}
else if (BROKEN.equals(actionMode)) {
colors.getItems().removeAll(brokenColors());
}
});
//- 'Refresh' button action
btnRefresh.setOnAction(e -> {
colors.getItems().setAll(colorsList());
hashCodes.getItems().setAll(colorsList()
.stream()
.map(ee -> ee.hashCode())
.collect(Collectors.toCollection(ArrayList::new)));
});
}
private ListChangeListener<Pair<String, Color>> listChangeListenerUsingIf() {
return c -> {
if (c.next()) {
System.out.println("Change variable class: " + c.getClass().getName());
if (c.wasRemoved()) {
System.out.println("Removing " + c.getRemovedSize() + " items");
c.getRemoved().forEach(e -> {
Integer hash = Integer.valueOf(e.hashCode());
hashCodes.getItems().remove(hash);
});
System.out.println("number of 'hash codes' after removal: " + hashCodes.getItems().size());
System.out.println();
}
if (c.wasAdded()) {
c.getAddedSubList().forEach(e -> {
if (hashCodes.getItems().stream().noneMatch(ee -> ee == e.hashCode()))
hashCodes.getItems().add(e.hashCode());
});
}
}
};
}
private ListChangeListener<Pair<String, Color>> listChangeListenerUsingWhile() {
return c -> {
while (c.next()) {
System.out.println("Change variable class: " + c.getClass().getName());
if (c.wasRemoved()) {
System.out.println("Removing " + c.getRemovedSize() + " items");
c.getRemoved().forEach(e -> {
Integer hash = Integer.valueOf(e.hashCode());
hashCodes.getItems().remove(hash);
});
System.out.println("number of 'hash codes' after removal: " + hashCodes.getItems().size());
System.out.println();
}
if (c.wasAdded()) {
c.getAddedSubList().forEach(e -> {
if (hashCodes.getItems().stream().noneMatch(ee -> ee == e.hashCode()))
hashCodes.getItems().add(e.hashCode());
});
}
}
};
}
private List<Pair<String, Color>> colorsList() {
return List.of(
new Pair<>("rot", Color.RED),
new Pair<>("orange", Color.ORANGE),
new Pair<>("gelb", Color.YELLOW),
new Pair<>("grün", Color.GREEN),
new Pair<>("blau", Color.BLUE),
new Pair<>("violett", Color.PURPLE),
new Pair<>("grau", Color.GRAY),
new Pair<>("schwarz", Color.BLACK));
}
private List<Pair<String, Color>> consecutiveColors() {
return List.of(
new Pair<>("gelb", Color.YELLOW),
new Pair<>("grün", Color.GREEN),
new Pair<>("blau", Color.BLUE));
}
private List<Pair<String, Color>> brokenColors() {
return List.of(
new Pair<>("rot", Color.RED),
new Pair<>("grün", Color.GREEN),
new Pair<>("blau", Color.BLUE));
}
public static void main(String[] args) {
launch(args);
}
}
Thanks in advance for any feedback.
[Edit in view of #Slaw's first comment]
This case raises several problems. #Slaw's first comment caused me to look at this differently. #Slaw is right in pointing out that using a while (c.next()) ... clause fixes the problem that is caused when a if (c.next())... clause is used.
When this is viewed holistically, however, there is more fundamental problem the led not so much to the use of the if (c.next()) clause, but disguised that error and made it very difficult to discover. This problem is the abysmal documentation for the ListChangeListener class.
I've modified the code for the sample app to include a second listener method that works properly (with a name change to the one that generated the error), together a comment as to why it was necessary and how the ListChangeListener and more particularly its Change companion, seem to work. The relevant parts of that comment are repeated below:
The listChangeListenerUsingIf() method returns a listener that uses an if (c.next()) ... statement to access the first change in the Change variable (c). For purposes of accessing the first change this is functionally equivalent to a while (c.next()) ... statement. However, because the Change variable may contain multiple 'remove' changes where each change is represented by a separate getRemoved() list, the if (c.next()) statement will catch only the first change while the while (c.next()) statement (which is used in the listChangeListenerUsingWhile() method) catches them all.
This case illustrates a serious flaw in the ListChangeListener API documentation because it fails to indicate that the Change variable may include multiple 'remove' changes and that each such change must be accessed in a separate iteration (e.g. the while (c.next()...).
In contrast, for 'add' changes (i.e. changes resulting from the addition of one or more items to the source list) the name of the method that returns the change(s) is getAddedSublist(). This clearly indicates that there may be more than one list of items that have been added, or similarly that the total items that have been 'added' by the change(s) represented by the Change variable may be included in more than one list; thus the use of the term sublist.
The flaw is illustrated further in the cautionary note in the API that reads as follows:
"[I]n case the change contains multiple changes of different type, these changes must be in the following order: permutation change(s), add or remove changes, update changes This is because permutation changes cannot go after add/remove changes as they would change the position of added elements. And on the other hand, update changes must go after add/remove changes because they refer with their indexes to the current state of the list, which means with all add/remove changes applied."
This is certainly useful information. However, the problems illustrated by the case at hand (i.e. different treatment based on whether the changed items are continguous in the source list) are just as significant as the situation addressed by the note; yet they are not mentioned.
A better understanding as to how the process works can be gained by running a system printout for the Change variable class (System.out.println("Change variable class: " + c.getClass().getSimpleName())) and compare the results yielded from changing the choice in the 'Action modes' combo box from 'consecutive' to 'broken'. For 'consecutive' (i.e. continguous), the class for the Change variable is ListChangeBuilder$SingleChange, for 'broken' (i.e. non-continguous) the class is ListChangeBuilder$IterableChange. These classes aren't well documented, which while regrettable is understandable inasmuch as they're private inner classes for restricted API. Interestingly, however, there is a public class MultipleAdditionAndRemovedChange (also restricted API) that appears to fit this case perfectly and is a bit more informative.
I hope this helps, and thanks to #Slaw for the useful input.
From the documentation of ListChangeListener.Change:
Represents a report of changes done to an ObservableList. The change may consist of one or more actual changes and must be iterated by calling the next() method [emphasis added].
In your implementation of ListChangeListener you have:
if (c.next()) {
// handle change...
}
This will only process a single change. You need to loop over (i.e. iterate) the changes in case there are multiple:
while (c.next()) {
// handle change...
}
Simply changing that one if to a while in your example fixes the problem you describe.
Here's an example showing how a bulk removal of non-contiguous elements results in multiple changes coalesced into a single ListChangeListener.Change object:
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
public class Main {
public static void main(String[] args) {
var list = FXCollections.observableArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
list.addListener(
(ListChangeListener<Integer>)
c -> {
System.out.println("----------BEGIN_CHANGE----------");
while (c.next()) {
// for example, assume c.wasRemoved() returns true
System.out.printf(
"Removed %d element(s): %s%n", c.getRemovedSize(), c.getRemoved());
}
System.out.println("-----------END_CHANGE-----------");
});
list.removeAll(1, 7, 3, 8, 2, 10);
}
}
And the output:
----------BEGIN_CHANGE----------
Removed 3 element(s): [1, 2, 3]
Removed 2 element(s): [7, 8]
Removed 1 element(s): [10]
-----------END_CHANGE-----------
If you're familiar with JDBC you'll notice the API for iterating a ListChangeListener.Change is similar to iterating a ResultSet.

Alternative for removed impl_isTreeVisible()

We are reliant on Node.impl_isTreeVisible() because isVisible does not work properly (or at least the way we want it to).
/**
* #treatAsPrivate implementation detail
* #deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
#Deprecated
public final boolean impl_isTreeVisible() {
return impl_treeVisibleProperty().get();
}
We have a custom Node which contains a Plot. This gets continuous data. We want to avoid to update the plot if it is not visible (still managed/rendered, but hidden).
If the node is placed on a tab which is not selected, hence it is not visible in the window, then using isVisible still returns true. This causes the Node on the selected tab to be rendred every time the plot is updated.
This will evaluate to true even though the node is not visible in the application window.
if (isVisible()) {
updatePlot()
}
So we have been using the following which works as we want it.
if (impl_isTreeVisible()) {
updatePlot()
}
However this will no longer work in Java 9 as such methods are removed. Is there a new approach to this in Java 9?
Update:
Looking at Java 9 source code for javafx.scene.Node I have found the method isTreeVisible(), which looks like a replacement for impl_isTreeVisible. However looking at the Javadoc I cannot find this isTreeVisible().
http://download.java.net/java/jdk9/docs/api/javafx/scene/Node.html
Trying with an example using isTreeVisible() will not compile with Java 9
Java9AppTest.java:50: error: cannot find symbol
if (text1.isTreeVisible()) {
^
symbol: method isTreeVisible()
location: variable text1 of type Text
Update2: Failed to see at first that isTreeVisible() is package private.
Update3: Taken another look at Node source code, I started to check out NodeHelper if could use it to get isTreeVisible(), however the package NodeHelper is not visible. Though using --add-exports for com.sun.javafx.scene to get access to NodeHelper works.
--add-exports javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED
Then I can read the state of isTreeVisible() of a Node.
final boolean isTreeVisible = NodeHelper.isTreeVisible(node);
Code Example
Contains two Tab, each with its own Text.
Has a Task that updates each Text.
Using isVisible() will update each text on both tabs.
Using impl_isTreeVisible() will only update the text that is truely visible.
It makes sense that Text should be updated, even if it is not visible. This is just to illustrate the problem. Replace Text with background process that does alot more CPU heavy work.
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Java9AppTest extends Application {
private Text text1, text2;
public static void main(String[] args) {
Java9AppTest.launch(args);
}
public void start(Stage stage) throws Exception {
TabPane root = new TabPane();
VBox box1 = new VBox();
text1 = new Text();
text1.setText("Hello World!");
text1.textProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("text1 changed from " + oldValue + " to " + newValue);
});
box1.getChildren().addAll(text1);
Tab tab1 = new Tab("Tab 1");
tab1.setContent(box1);
VBox box2 = new VBox();
text2 = new Text();
text2.setText("Another Hello World!");
text2.textProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("text2 changed from " + oldValue + " to " + newValue);
});
box2.getChildren().add(text2);
Tab tab2 = new Tab("Tab 2");
tab2.setContent(box2);
root.getTabs().addAll(tab1, tab2);
Task<Void> task = new Task<Void>() {
/* (non-Javadoc)
* #see javafx.concurrent.Task#call()
*/
#Override
protected Void call() throws Exception {
final String oldText = "Hello World!";
final String newText = "New Hello World!";
while (true) {
if (text1.isVisible()) {
if (text1.getText().equals(oldText)) {
text1.setText(newText);
} else {
text1.setText(oldText);
}
}
if (text2.isVisible()) {
if (text2.getText().equals(oldText)) {
text2.setText(newText);
} else {
text2.setText(oldText);
}
}
Thread.sleep(2000);
}
}
};
stage.setScene(new Scene(root));
stage.setWidth(200);
stage.setHeight(200);
stage.setTitle("JavaFX 9 Application");
stage.show();
Thread thread = new Thread(task, "Task");
thread.start();
}
}
I suggest adding a property to your node, that controls if you want to update the plot. So instead of if (impl_isTreeVisible()) { just have if (shouldUpdate) {. Upon tab selection changes, just toggle the property. So in essence your TabPane would control if the plot is updated.
Alternatively you could pass the TabPane to your node and query the selected tab: tabPane.getSelectionModel().getSelectedIndex(). This, however means that your node must know on which tab it resides.
A Tab has a property selected, bind that property to an update property of your plot, which determines if you redraw your plot.
In your control (or its skin) add a listener to the update property of the plot, where you pause or resume listening to your input source, or pause or resume the timer that gets the data.
This solution does not add additional dependencies to the object graph, the type of container it should be in and enables you to create more complex bindings if necessary (like a pause button), and eases testing as this property is controllable in a standalone manner.
Depending on the data source implementation this solution can also pause your data source if it determines that there are no listeners processing your data actively.

JavaFX - creating custom dialogs using fxml

I'm new in javafx and I was trying to create custom dialogs/alerts. The thing is that I'm using Scene Builder to design the GUI, and I want to modify the dialog each time I load the fxml file (i.e change the title, label text, etc.), so I wanted to know if there's a way to send parameters and modify the stage/scene, or any other way I can achieve this.
To be more specific, let's say there's an error I want to handle anywhere in my program, so I load a new fxml file that represents the error dialog I created, and I modify the components inside it, depending on the type of error I need to handle, similar to, for example, JOptionPane.showMessageDialog(...) in swing.
For the use case you describe, you can just use the Dialog API, or the specialized Alert class that is part of that.
For the more general question you ask:
I wanted to know if there's a way to send parameters and change the stage/scene
the way to do this is to use the custom component mechanism described in the documentation.
In short, make a subclass of the UI type you need that loads the FXML file, and defines the properties you need, e.g.
public class ExceptionPane extends BorderPane {
private final ObjectProperty<Exception> exception ;
public ObjectProperty<Exception> exceptionProperty() {
return exception ;
}
public final Exception getException() {
return exceptionProperty().get();
}
public final void setException(Exception exception) {
exceptionProperty().set(exception);
}
#FXML
private final TextArea stackTrace ;
#FXML
private final Label message ;
public ExceptionPane() throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml"));
loader.setRoot(this);
loader.setController(this);
loader.load();
exception.addListener((obs, oldException, newException) -> {
if (newException == null) {
message.setText(null);
stackTrace.setText(null);
} else {
message.setText(newException.getMessage());
StringWriter sw = new StringWriter();
newException.printStackTrace(new PrintWriter(sw));
stackTrace.setText(sw.toString());
}
});
}
}
Then define the FXML using a "dynamic root":
<!-- imports etc -->
<fx:root type="BorderPane" ...>
<center>
<TextArea fx:id="stackTrace" editable="false" wrapText="false" />
</center>
<top>
<Label fx:id="message" />
</top>
</fx:root>
Now you can use this directly in either Java or in FXML:
try {
// some code...
} catch (Exception exc) {
ExceptionPane excPane = new ExceptionPane();
excPane.setException(exc);
Stage stage = new Stage();
stage.setScene(new Scene(excPane));
stage.show();
}
or
<fx:define fx:id="exc"><!-- define exception somehow --></fx:define>
<ExceptionPane exception="${exc}" />

TableCell.setText(String) doesn't set the data value associated with the cell

In my particular case I have a custom implementation of a TableCell that contains a Button. This button invokes a method that returns a String to be displayed instead of the button. The visual change is done by setting the graphic in the cell to null and setting the text to the String, using TableCell.setText(String).
What I've realized - and worked around so far, is that TableCell.setText(String) doesn't change the data value associated with the cell in the TableView. It just changes the visual representation of the cell. The underlying data structure is in my case a ObservableList<String> that represents a row, and each element in the list is, of course, cell data.
My current solution is to set the underlying value doing this:
getTableView().getItems().get(getIndex()).set(getTableView().getColumns().indexOf(getTableColumn()), "Value");
And this works fine. But I mean, the code is barely readable.
It seems like the data in the TableView and the TableCell are entirely separated, since you need to access the TableView to set the underlying data for a cell. There is a TableCell.getItem() to get the data value, but there's no setItem(String) method to set it.
I hope I explained my issue good enough.
Is there a better and prettier way to do this? Why doesn't just `TableCell.setText(String) change the data value as well?
Edit: I'll explain what I am trying to implement:
I basically have a table where one column contains a button that will load some arbitrary data to the column when pressed. Once the data has been loaded, the button is removed from the column and the data is displayed instead. That is basically it. This works fine unless the table is sorted/filtered. Here's a MCVE of my implementation:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
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.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
public class MCVE extends Application {
private final BooleanProperty countLoading = new SimpleBooleanProperty(this, "countLoading", false);
#Override
public void start(Stage stage) {
int numOfCols = 3;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 100; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
if (i == 2) {
final int j = i;
table.getColumns().add(addColumn(i, "Column " + i, e -> new QueueCountCell(j, countLoading)));
} else {
table.getColumns().add(addColumn(i, "Column " + i, null));
}
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a simple column.
*/
private TableColumn<ObservableList<String>, String> addColumn(int index, String name,
Callback<TableColumn<ObservableList<String>, String>, TableCell<ObservableList<String>, String>> callback) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(e -> new SimpleStringProperty(e.getValue().get(index)));
if (callback != null) {
col.setCellFactory(callback);
}
return col;
}
public static void main(String[] args) {
launch();
}
class QueueCountCell extends TableCell<ObservableList<String>, String> {
private final Button loadButton = new Button("Load");
public QueueCountCell(int colIndex, BooleanProperty countLoading) {
countLoading.addListener((obs, oldValue, newValue) -> {
if (newValue) {
loadButton.setDisable(true);
} else {
if (getIndex() >= 0 && getIndex() < this.getTableView().getItems().size()) {
loadButton.setDisable(false);
}
}
});
final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, e -> setText("Loading .")),
new KeyFrame(Duration.millis(500), e -> setText("Loading . .")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading . . .")),
new KeyFrame(Duration.millis(1500)));
timeline.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> {
new Thread(new Task<String>() {
#Override
public String call() throws InterruptedException {
// Simlute task working.
Thread.sleep(3000);
return "5";
}
#Override
public void running() {
setGraphic(null);
timeline.play();
countLoading.set(true);
}
#Override
public void succeeded() {
timeline.stop();
countLoading.set(false);
setText(getValue());
}
#Override
public void failed() {
timeline.stop();
countLoading.set(false);
setGraphic(loadButton);
setText(null);
this.getException().printStackTrace();
}
}).start();
});
}
#Override
public final void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(loadButton);
}
}
}
}
Background: MVC
Much of JavaFX is designed around a Model-View-Controller (MVC) pattern. This is a loosely-defined pattern with many variants, but the basic idea is that there are three components:
Model: an object (or objects) that represent the data. The Model knows nothing about how the data is presented to the user.
View: an object that presents the data to the user. The view does not do any logical processing or store the data; it just knows how to convert the data to some kind of presentation for the user.
Controller: an object that modifies the data in the model, often (though not exclusively) in response to user input.
There are several variants of this pattern, including MVP, MVVM, supervising controller, passive view, and others, but the unifying theme in all of them is that there is a separation between the view, which simply presents data but does not otherwise "know" what the data is, and the model, which stores the state (data) but knows nothing about how it might be presented. The usually-cited motivation for this is the ability to have multiple views of the same data which have no need to refer to each other.
In the "classical" implementation of this, the view "observes" the model via some kind of subscriber-notification pattern (e.g. an observer pattern). So the view will register with the model to be notified of changes to the data, and will repaint accordingly. Often, since the controller relies on event listeners on the components in the view, the controller and view are tightly coupled; however there is always clear separation between the view and the model.
The best reference I know for learning more about this is Martin Fowler.
Background: JavaFX Virtualized Controls
JavaFX has a set of "virtualized controls", which includes ListView, TableView, TreeView, and TreeTableView. These controls are designed to be able to present large quantities of data to the user in an efficient manner. The key observation behind the design is that data is relatively inexpensive to store in memory, whereas the UI components (which typically have hundreds of properties) consume a relatively large amount of memory and are computationally expensive (e.g. to perform layout, apply style, etc). Moreover, in a table (for example) with a large amount of backing data, only a small proportion of those data are visible at any time, and there is no real need for UI controls for the remaining data.
Virtualized controls in JavaFX employ a cell rendering mechanism, in which "cells" are created only for the visible data. As the user scrolls around the table, the cells are reused to display data that was previously not visible. This allows the creation of a relatively small number of cells even for extremely large data sets: the number of (expensive) cells created is basically constant with respect to the size of the data. The Cell class defines an updateItem(...) method that is invoked when the cell is reused to present different data. All this is possible because the design is built on MVC principles: the cell is the view, and the data is stored in the model. The documentation for Cell has details on this.
Note that this means that you must not use the cell for any kind of data storage, because when the user scrolls in the control, that state will be lost. General MVC principles dictate that this is what you should do anyway.
The code you posted doesn't work correctly, as it violates these rules. In particular, if you click one of the "Load" buttons, and then scroll before the loading is complete, the cell that is performing the loading will now be referring to the wrong item in the model, and you end up with a corrupted view. The following series of screenshots occurred from pressing "Load", taking a screenshot, scrolling, waiting for the load to complete, and taking another screenshot. Note the value appears to have changed for an item that is different to the item for which "Load" was pressed.
To fix this, you have to have a model that stores all of the state of the application: you cannot store any state in the cells. It is a general truth in JavaFX that in order to make the UI code elegant, you should start with a well-defined data model. In particular, since your view (cell) changes when the data is in the process of loading, the "loading state" needs to be part of the model. So each item in each row in your table is represented by two pieces of data: the actual data value (strings in your case), and the "loading state" of the data.
So I would start with a class that represents that. You could just use a String for the data, or you could make it more general by making it a generic class. I'll do the latter. A good implementation will also keep the two states consistent: if the data is null and we have not explicitly stated it is loading, we consider it not loaded; if the data is non-null, we consider it loaded. So we have:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
public class LazyLoadingData<T> {
public enum LoadingState { NOT_LOADED, LOADING, LOADED }
private final ObjectProperty<T> data = new SimpleObjectProperty<>(null);
private final ReadOnlyObjectWrapper<LoadingState> loadingState
= new ReadOnlyObjectWrapper<>(LoadingState.NOT_LOADED);
public LazyLoadingData(T data) {
// listeners to keep properties consistent with each other:
this.data.addListener((obs, oldData, newData) -> {
if (newData == null) {
loadingState.set(LoadingState.NOT_LOADED);
} else {
loadingState.set(LoadingState.LOADED);
}
});
this.loadingState.addListener((obs, oldState, newState) -> {
if (newState != LoadingState.LOADED) {
this.data.set(null);
}
});
this.data.set(data);
}
public LazyLoadingData() {
this(null);
}
public void startLoading() {
loadingState.set(LoadingState.LOADING);
}
public final ObjectProperty<T> dataProperty() {
return this.data;
}
public final T getData() {
return this.dataProperty().get();
}
public final void setData(final T data) {
this.dataProperty().set(data);
}
public final ReadOnlyObjectProperty<LoadingState> loadingStateProperty() {
return this.loadingState.getReadOnlyProperty();
}
public final LazyLoadingData.LoadingState getLoadingState() {
return this.loadingStateProperty().get();
}
}
The model here will just be an ObservableList<List<LazyLoadingData<String>>>, so each cell is a LazyLoadingData<String> and each row is a list of them.
To make this properly MVC, let's have a separate controller class which has a way of updating data in the model:
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javafx.concurrent.Task;
public class LazyLoadingDataController {
// data model:
private final List<List<LazyLoadingData<String>>> data ;
private final Random rng = new Random();
private final Executor exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
public LazyLoadingDataController(List<List<LazyLoadingData<String>>> data) {
this.data = data ;
}
public void loadData(int column, int row) {
Task<String> loader = new Task<String>() {
#Override
protected String call() throws InterruptedException {
int value = rng.nextInt(1000);
Thread.sleep(3000);
return "Data: "+value;
}
};
data.get(row).get(column).startLoading();
loader.setOnSucceeded(e -> data.get(row).get(column).setData(loader.getValue()));
exec.execute(loader);
}
}
Now our cell implementation is pretty straightforward. The only tricky part is that each item has two properties, and we actually need to observe both of those properties and update the cell if either of them changes. We need to be careful to remove listener from items the cell is no longer displaying. So the cell looks like:
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.util.Duration;
public class LazyLoadingDataCell<T>
extends TableCell<List<LazyLoadingData<T>>, LazyLoadingData<T>>{
private final Button loadButton = new Button("Load");
private final Timeline loadingAnimation = new Timeline(
new KeyFrame(Duration.ZERO, e -> setText("Loading")),
new KeyFrame(Duration.millis(500), e -> setText("Loading.")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading..")),
new KeyFrame(Duration.millis(1500), e -> setText("Loading..."))
);
public LazyLoadingDataCell(LazyLoadingDataController controller, int columnIndex) {
loadingAnimation.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> controller.loadData(columnIndex, getIndex()));
// listener for observing either the dataProperty()
// or the loadingStateProperty() of the current item:
ChangeListener<Object> listener = (obs, oldState, newState) -> doUpdate();
// when the item changes, remove and add the listener:
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.dataProperty().removeListener(listener);
oldItem.loadingStateProperty().removeListener(listener);
}
if (newItem != null) {
newItem.dataProperty().addListener(listener);
newItem.loadingStateProperty().addListener(listener);
}
doUpdate();
});
}
#Override
protected void updateItem(LazyLoadingData<T> item, boolean empty) {
super.updateItem(item, empty);
doUpdate();
}
private void doUpdate() {
if (isEmpty() || getItem() == null) {
setText(null);
setGraphic(null);
} else {
LazyLoadingData.LoadingState state = getItem().getLoadingState();
if (state == LazyLoadingData.LoadingState.NOT_LOADED) {
loadingAnimation.stop();
setText(null);
setGraphic(loadButton);
} else if (state == LazyLoadingData.LoadingState.LOADING) {
setGraphic(null);
loadingAnimation.play();
} else if (state == LazyLoadingData.LoadingState.LOADED) {
loadingAnimation.stop();
setGraphic(null);
setText(getItem().getData().toString());
}
}
}
}
Note how
The cell contains no state. The fields in the cell are entirely related to the display of data (a button and an animation).
The action of the button doesn't (directly) change anything in the view. It simply tells the controller to update the data in the model. Because the cell (view) is observing the model, when the model changes, the view updates.
The model also changes independently of user action, when the task in the controller completes. Because the view is observing the model for changes, it updates automatically.
Finally an example using this. There is not much unexpected here, we just create a model (ObservableList of List<LazyLoadingData<String>>), create a controller, and then a table with some columns.
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class LazyLoadingTableExample extends Application {
private final int numCols = 3 ;
private final int numRows = 100 ;
#Override
public void start(Stage primaryStage) {
TableView<List<LazyLoadingData<String>>> table = new TableView<>();
// data model:
ObservableList<List<LazyLoadingData<String>>> data
= FXCollections.observableArrayList();
table.setItems(data);
LazyLoadingDataController controller = new LazyLoadingDataController(data);
// build data:
for (int i = 0; i < numRows; i++) {
ObservableList<LazyLoadingData<String>> row
= FXCollections.observableArrayList();
for (int j = 0 ; j < numCols - 1 ; j++) {
row.add(new LazyLoadingData<>("Cell ["+j+", "+i+"]"));
}
row.add(new LazyLoadingData<>());
data.add(row);
}
for (int i = 0 ; i < numCols ; i++) {
table.getColumns().add(createColumn(controller, i));
}
Scene scene = new Scene(table, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>>
createColumn(LazyLoadingDataController controller, int columnIndex) {
TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>> col
= new TableColumn<>("Column "+columnIndex);
col.setCellValueFactory(cellData ->
new SimpleObjectProperty<>(cellData.getValue().get(columnIndex)));
col.setCellFactory(tc ->
new LazyLoadingDataCell<>(controller, columnIndex));
return col ;
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX ChoiceBox add separator with type safety

I'm looking to add a separator into a choice box and still retain the type safety.
On all of the examples I've seen, they just do the following:
ChoiceBox<Object> cb = new ChoiceBox<>();
cb.getItems().addAll("one", "two", new Separator(), "fadfadfasd", "afdafdsfas");
Has anyone come up with a solution to be able to add separators and still retain type safety?
I would expect that if I wanted to add separators, I should be able do something along the following:
ChoiceBox<T> cb = new ChoiceBox<T>();
cb.getSeparators().add(1, new Separator()); // 1 is the index of where the separator should be
I shouldn't have to sacrifice type safety just to add separators.
As already noted, are Separators only supported if added to the items (dirty, dirty). To support them along the lines expected in the question, we need to:
add the notion of list of separator to choiceBox
make its skin aware of that list
While the former is not a big deal, the latter requires a complete re-write (mostly c&p) of its skin, as everything is tightly hidden in privacy. If the re-write has happened anyway, then it's just a couple of lines more :-)
Just for fun, I'm experimenting with ChoiceBoxX that solves some nasty bugs in its selection handling, so couldn't resist to try.
First, add support to the ChoiceBoxx itself:
/**
* Adds a separator index to the list. The separator is inserted
* after the item with the same index. Client code
* must keep this list in sync with the data.
*
* #param separator
*/
public final void addSeparator(int separator) {
if (separatorsList.getValue() == null) {
separatorsList.setValue(FXCollections.observableArrayList());
}
separatorsList.getValue().add(separator);
};
Then some changes in ChoiceBoxXSkin
must listen to the separatorsList
must expect index-of-menuItem != index-of-choiceItem
menuItem must keep its index-of-choiceItem
At its simplest, the listener re-builds the popup, the menuItem stores the dataIndex in its properties and all code that needs to access a popup by its dataIndex is delegated to a method that loops through the menuItems until it finds one that fits:
protected RadioMenuItem getMenuItemFor(int dataIndex) {
if (dataIndex < 0) return null;
int loopIndex = dataIndex;
while (loopIndex < popup.getItems().size()) {
MenuItem item = popup.getItems().get(loopIndex);
ObservableMap<Object, Object> properties = item.getProperties();
Object object = properties.get("data-index");
if ((object instanceof Integer) && dataIndex == (Integer) object) {
return item instanceof RadioMenuItem ? (RadioMenuItem)item : null;
}
loopIndex++;
}
return null;
}
Well you can work around it by creating an interface and then subclassing Separator to implement this interface:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class ChoiceBoxIsSafe extends Application {
interface FruitInterface { }
static public class Fruit implements FruitInterface {
private StringProperty name = new SimpleStringProperty();
Fruit(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.get();
}
}
static public class FruitySeparator extends Separator implements FruitInterface { }
#Override
public void start(Stage primaryStage) throws Exception {
GridPane grid = new GridPane();
grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10));
ChoiceBox<FruitInterface> cb = new ChoiceBox<>();
cb.getItems().addAll(new Fruit("Apple"), new Fruit("Orange"), new FruitySeparator(), new Fruit("Peach"));
Text text = new Text("");
ReadOnlyObjectProperty<FruitInterface> selected = cb.getSelectionModel().selectedItemProperty();
text.textProperty().bind(Bindings.select(selected, "name"));
grid.add(cb, 0, 0);
grid.add(text, 1, 0);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
but that is hardly an "elegant" solution and cannot be done in all cases (e.g. ChoiceBox<String>).
From the implementation of ChoiceBox it certainly looks like it wasn't a good idea to treat Separators like items in the ChoiceBox :-(.
FOR THE REST OF US:
There is a MUCH easier way to do this using code (there are easy ways to do it using FXML too, doing it in code offers more flexibility).
You simply create an ObservableList, then populate it with your items, including the separator then assign that list to the ChoiceBox like this:
private void fillChoiceBox(ChoiceBox choiceBox) {
ObservableList items = FXCollections.observableArrayList();
items.add("one");
items.add("two");
items.add("three");
items.add(new Separator());
items.add("Apples");
items.add("Oranges");
items.add("Pears");
choiceBox.getItems().clear();
choiceBox.getItems().addAll(items);
}

Resources