OK, let me take a stab at something, though I still don't know if I really understand the question. I think you want to get a subset of the scene graph by getting child nodes of different Parent
subclasses in different ways (i.e. not necessarily just by calling Parent.getChildrenUnmodifiable()
). So if it's a simple Pane
, you would just call getChildren()
, but if it's a TabPane
, you would get each Tab
and get the content of each tab, forming a collection from those. (And similarly for other "container-type controls", such as SplitPane
, etc.) If it's a "simple" control, you don't regard it as having any child nodes (even though, behind the scenes, a Button
contains a Text
, for example).
So I think you could perhaps do this by building a typesafe heterogeneous container (see Josh Bloch's Effective Java) that mapped specific node types N
to a Function<N, List<Node>>
. The function would define how to retrieve child nodes for that type.
This might look like
public class ChildRetrievalMapping {
public static final ChildRetrievalMapping DEFAULT_INSTANCE = new ChildRetrievalMapping() ;
static {
// note the order of insertion is important: start with the more specific type
DEFAULT_INSTANCE.put(TabPane.class, tabPane ->
tabPane.getTabs().stream().map(Tab::getContent).collect(Collectors.toList()));
DEFAULT_INSTANCE.put(SplitPane.class, SplitPane::getItems);
// others...
// default behavior for "simple" controls, just return empty list:
DEFAULT_INSTANCE.put(Control.class, c -> Collections.emptyList());
// default behavior for non-control parents, return getChildrenUnmodifiable:
DEFAULT_INSTANCE.put(Parent.class, Parent::getChildrenUnmodifiable);
// and for plain old node, just return empty list:
DEFAULT_INSTANCE.put(Node.class, n -> Collections.emptyList());
}
private final Map<Class<?>, Function<? ,List<Node>>> map = new LinkedHashMap<>();
public <N extends Node> void put(Class<N> nodeType, Function<N, List<Node>> childRetrieval) {
map.put(nodeType, childRetrieval);
}
@SuppressWarnings("unchecked")
public <N extends Node> Function<N, List<Node>> getChildRetrieval(Class<N> nodeType) {
return (Function<N, List<Node>>) map.get(nodeType);
}
@SuppressWarnings("unchecked")
public List<Node> firstMatchingList(Node n) {
for (Class<?> type : map.keySet()) {
if (type.isInstance(n)) {
return getChildRetrieval((Class<Node>) type).apply(n);
}
}
return Collections.emptyList();
}
}
Now you can just call childRetrievalMapping.findFirstMatchingList(node);
and it gets the list of children in the sense defined by the first type in the map which matches the node. So, using the DEFAULT_INSTANCE
, if you passed it a TabPane
, it would get all the content nodes; if you passed it a SplitPane
, it would get the items; if you passed it another type of control, it would return an empty list, etc.
Here's an example of using this. This just builds a scene graph, and then when you press the button, it traverses it, getting just the "simple" nodes defined by the strategies in the above class. (And then it selects all instances of Labeled
and passes the result of getText()
to the system console.) Note how it (deliberately) avoids the labels that are part of the implementation of the tabs themselves, which a naïve root.lookupAll(".labeled")
would not do.
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class PerformActionOnNodeTypes extends Application {
@Override
public void start(Stage primaryStage) {
VBox root = new VBox(5,
new Label("Label 1"),
new HBox(5, new Label("Label 2"), new Button("Button 1")),
new HBox(5, new TextField("Some text"), new ComboBox<String>()),
new TabPane(new Tab("Tab 1", new VBox(new Label("Label in tab 1"))),
new Tab("Tab 2", new StackPane(new Button("Button in tab 2")))));
Button button = new Button("Show labeled's texts");
button.setOnAction(e -> {
List<Node> allSimpleNodes = new ArrayList<>();
findAllSimpleNodes(allSimpleNodes, root);
doAction(allSimpleNodes, Labeled.class, (Labeled l) -> System.out.println(l.getText()));
});
root.setAlignment(Pos.CENTER);
BorderPane.setAlignment(button, Pos.CENTER);
Scene scene = new Scene(new BorderPane(root, null, null, button, null), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void findAllSimpleNodes(List<Node> allSimpleNodes, Node n) {
List<Node> children = ChildRetrievalMapping.DEFAULT_INSTANCE.firstMatchingList(n);
allSimpleNodes.addAll(children);
for (Node child : children) {
findAllSimpleNodes(allSimpleNodes, child);
}
}
private <T> void doAction(Collection<Node> nodes, Class<T> type, Consumer<T> action) {
nodes.stream()
.filter(type::isInstance)
.map(type::cast)
.forEach(action);
}
public static class ChildRetrievalMapping {
public static final ChildRetrievalMapping DEFAULT_INSTANCE = new ChildRetrievalMapping() ;
static {
// note the order of insertion is important: start with the more specific type
DEFAULT_INSTANCE.put(TabPane.class, tabPane ->
tabPane.getTabs().stream().map(Tab::getContent).collect(Collectors.toList()));
DEFAULT_INSTANCE.put(SplitPane.class, SplitPane::getItems);
// others...
// default behavior for "simple" controls, just return empty list:
DEFAULT_INSTANCE.put(Control.class, c -> Collections.emptyList());
// default behavior for non-control parents, return getChildrenUnmodifiable:
DEFAULT_INSTANCE.put(Parent.class, Parent::getChildrenUnmodifiable);
// and for plain old node, just return empty list:
DEFAULT_INSTANCE.put(Node.class, n -> Collections.emptyList());
}
private final Map<Class<?>, Function<? ,List<Node>>> map = new LinkedHashMap<>();
public <N extends Node> void put(Class<N> nodeType, Function<N, List<Node>> childRetrieval) {
map.put(nodeType, childRetrieval);
}
@SuppressWarnings("unchecked")
public <N extends Node> Function<N, List<Node>> getChildRetrieval(Class<N> nodeType) {
return (Function<N, List<Node>>) map.get(nodeType);
}
@SuppressWarnings("unchecked")
public List<Node> firstMatchingList(Node n) {
for (Class<?> type : map.keySet()) {
if (type.isInstance(n)) {
return getChildRetrieval((Class<Node>) type).apply(n);
}
}
return Collections.emptyList();
}
}
public static void main(String[] args) {
launch(args);
}
}
I'm not sure if that's what you were wanting to do, and if so there may be more elegant ways to approach it. But I think declaring a strategy like this for particular types is quite a bit nicer than a big switch on types, and it leaves the option of configuring it to have the specific rules you want.