You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I like to create builders for more than just single nodes, e.g. if you are building a more complex group of nodes like an HBox of multiple buttons. For those types of builders, I like to ensure that I have references to all included nodes, e.g. a reference to all buttons. I know this can be done with something like ids or style class lookups, but having a typed reference makes the job easier.
It essentially gives you a choice to build the structure if you're interested in having references to all child nodes. If not, then you can just build it normally like other builders. It would be nice if this had proper support because my implementation is a bit shaky.
The last link is broken, so not entirely sure what you are doing.
There is nothing stopping you from making a group of nodes with a single builder AFAIK; I mean the pane builders do exactly that, except the nodes are supplied. You can easily make a method that accepts a count, which then adds that number of children for example. Builders can also store information locally in fields if they want to (most however don't).
When it comes to references after building completes (ie. for the user that defines the UI), they will either have to use a variable and break from the structured style themselves, or be satisfied with things like NodeEventHandlers or Triggers.
I may be missing something though, as I can't see your last link.
Can you explain what those advantages are and why you need that? I've never had the need to manipulate a sub-node directly. In my view, a sub-node is owned by its parent (or skin) and manipulating it directly in any way is a violation of encapsulation. Instead the container should selectively expose actions, properties and events, and not allow access to its internals (exposing a Node basically exposes everything).
I've been trying to see what you are doing here, and it seems you are creating a fancy button. The closest analogy in a code base I use is this:
private Node createTitledPane(String title, Node content, Node contentSummary) {
return FX.titledPane("node")
.graphic(titleAndControl(title, Panes.hbox("trailing-region").nodes(
contentSummary, // automatically elided if null
Panes.hbox("controls").nodes(
FX.menuButton().style("gear").text("\u2699").onShowing((c, event) -> {
// add menu items here
}),
FX.button().text("\u274C").onAction(e -> remove(indexOfChild(content))))) // an "X" close button
)
)
.content(content);
}
// Helper to create a graphic for a titled pane that has a title followed by a gap followed by a control
private static Node titleAndControl(String title, Object control) {
Label label = new Label(title);
HBox.setGrow(label, Priority.ALWAYS);
label.setMaxWidth(10000);
return Panes.hbox("node").alignment(Pos.CENTER_LEFT).nodes(label, control);
}
This creates the titled pane you see in this image:
I am doing that because I extended the builder use to be more than just simple controls. It's more like compound controls that are made up of multiple individual nodes.
In theory, you could wire everything up so that everything can be manipulated directly, yes. But in practice it turned out to be much easier to just expose it via a view / structure. It's the same problem with skins in JavaFX. If you want to modify some internal thing of a skin, but it's not exposed properly, you hope that there is at least a style class associated with it to somehow look it up. Otherwise, you need an entire new skin or weird hack to just to get some small Label behave a bit differently than intended by the original skin.
In your example, let's say I want to have the title pane title be an ObservableValue. You could add that as an overloaded method or just modify the original one. After you do that, you might also want to have a graphic for the title pane label. Ok, you add a way to pass a graphic as well. Then, someone wants to change the graphic layout, etc. At some point, using views becomes much easier than to just add every possibility of customization.
Right now, the primary use of builders is to build individual nodes. You configure certain properties like text, graphic, etc. for a label builder and that works out fine. A structure builder primarily takes other builders as parameters that make up the larger structure.
If you want to create larger structures that are made out of many nodes/builders, the current system doesn't work that well. You have the same issue in your example, you have to create helper methods to build the node structures you want. I wanted to avoid these helper methods and instead primarily use class instances for that. Because helper methods don't scale very well when you add more and more parameters over time. And if you use custom builder class instances to replace the helper methods, you still lose the information about the individual nodes that make up the generated structure.
Right now, the primary use of builders is to build individual nodes. You configure certain properties like text, graphic, etc. for a label builder and that works out fine. A structure builder primarily takes other builders as parameters that make up the larger structure.
That's the primary use case for a builder. A builder that has other builders as parameters is a pane, and panes do exactly that:
Panes.hbox(builder, builder, builder)
There is no need to call build.
If you want to create larger structures that are made out of many nodes/builders, the current system doesn't work that well. You have the same issue in your example, you have to create helper methods to build the node structures you want. I wanted to avoid these helper methods and instead primarily use class instances for that. Because helper methods don't scale very well when you add more and more parameters over time. And if you use custom builder class instances to replace the helper methods, you still lose the information about the individual nodes that make up the generated structure.
It's only a helper method because I didn't deem that pressing enough to convert to its own builder or custom control. The only reason this exists at all is because TitledPane is bugged and doesn't render the graphic correctly if you use a label and graphic separately (so I had to instead make my own label + graphic inside graphic of TitledPane). This issue is fixed in later FX versions as I fixed it myself.
As for creating larger structures, you can do that easily. Here's a larger structure:
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import org.int4.fx.builders.FX;
import org.int4.fx.builders.Panes;
import org.int4.sdui.invokeai.Model;
public class UpscalingPane extends VBox {
public final UpscalingModel model = new UpscalingModel();
public UpscalingPane() {
getStyleClass().add("upscaling-pane");
getChildren().addAll(
Panes.hbox("form", "control-group").nodes(
Panes.hbox().nodes(
"Method",
FX.comboBox()
.model(model.upscalingModel)
.cellFactory(ModelFieldCell::new)
.buttonCell(new ModelFieldCell())
),
Panes.hbox().nodes(
"Factor",
Panes.hbox().nodes(
FX.slider().showTickMarks(2.0, 0).model(model.upscalingFactor),
FX.textField().style("upscaling-factor", "sz1").model(model.upscalingFactor)
)
)
)
.build()
);
}
private static final class ModelFieldCell extends ListCell<Model> {
@Override
protected void updateItem(Model model, boolean empty) {
super.updateItem(model, empty);
setText(empty ? "" : model.name());
}
}
}
Note that it is encapsulated, and the outside world can create as many of these as they want, and all they have to do is link up the public field. This can be converted to a builder as well, but haven't gotten around to it. Another example of a nicely encapsulated control (partially done):
/**
* A specialized marker overlay pane that displays validation markers on nodes.
* <p>
* This pane listens for {@link ValidationEvent#VALIDATION_CHANGED} events
* and automatically adds or removes markers on target nodes depending on their
* validation state.
* <p>
* Each marker consists of a small red circle with a white "X" drawn on top,
* automatically scaled according to the node's font size.
*/
public class ValidationMarkerPane extends AbstractMarkerPane {
/**
* Creates a builder for a {@link ValidationMarkerPane}, initialising
* it with the given style classes.
*
* @param styleClasses the style classes, cannot be {@code null} but can be empty
* @return a fluent builder, never {@code null}
* @throws NullPointerException if any argument is {@code null}
*/
public static ValidationMarkerPaneBuilder of(String... styleClasses) {
return new ValidationMarkerPaneBuilder(FXContext.global(), styleClasses);
}
/**
* Creates a new validation marker overlay pane.
*/
public ValidationMarkerPane() {
getStyleClass().add("validation-marker-pane");
addEventFilter(ValidationEvent.VALIDATION_CHANGED, e -> {
if(e.getTarget() instanceof Node node) {
if(e.isValid()) {
removeMarker(node);
}
else {
FontGroup marker = new FontGroup();
marker.setPickOnBounds(false);
marker.getStyleClass().add("marker");
marker.getChildren().addAll(
Panes.stack("background").ignoreBounds().build(),
Panes.stack("foreground").ignoreBounds().build()
);
marker.setFont(resolveFont(node));
String message = switch(e.getDescriptor().key()) {
case "domain.notNull" -> "Cannot be empty";
case "domain.bounded" -> "Must be between {0} and {1}";
case "domain.invalid" -> "Incorrect type";
default -> "Invalid";
};
putMarker(
node,
marker,
new Tooltip(MessageFormat.format(message, e.getDescriptor().args()))
);
}
}
});
}
@Override
public String getUserAgentStylesheet() {
return StyleSheets.inline(
"""
.validation-marker-pane {
-fx-padding: 0;
-fx-background-color: rgba(255,0,0,0.2);
-fx-margin: 0;
}
.validation-marker-pane .overlay .marker {
-fx-opacity: 0.8;
-fx-scale-x: 1em;
-fx-scale-y: 1em;
-fx-translate-x: -0.25em;
-fx-translate-y: 0.25em;
}
.validation-marker-pane .overlay .marker .foreground {
-fx-min-width: 0.7;
-fx-min-height: 0.7;
-fx-translate-x: 0.15;
-fx-translate-y: 0.15;
-fx-background-color: white;
// a thick cross:
-fx-shape: "
M -0.35,-0.5
L -0.5,-0.35
L -0.15,0
L -0.5,0.35
L -0.35,0.5
L 0,0.15
L 0.35,0.5
L 0.5,0.35
L 0.15,0
L 0.5,-0.35
L 0.35,-0.5
L 0,-0.15
Z
";
}
.validation-marker-pane .overlay .marker .background {
-fx-min-width: 1;
-fx-min-height: 1;
-fx-background-color: red;
// a circle:
-fx-shape: "
M 0,0
m -0.5,0
a 0.5,0.5 0 1,0 1,0
a 0.5,0.5 0 1,0 -1,0
";
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 1.0), 0.35, 0.4, 0.1, 0.1);
}
"""
);
}
@Override
protected Point2D computeMarkerPosition(Node node) {
Bounds b = node.getLayoutBounds();
return new Point2D(b.getMaxX(), b.getMinY());
}
private static Font resolveFont(Node node) {
Node current = node;
while(current != null) {
@SuppressWarnings("unchecked") // bit dirty, assume all CssMetaData properties are fonts :)
List<CssMetaData<Node, Font>> cssMetaDataList = (List<CssMetaData<Node, Font>>)(List<?>)current.getCssMetaData();
for(CssMetaData<Node, Font> md : cssMetaDataList) {
if(md.getProperty().equals("-fx-font")) { // this is a real font property
return md.getStyleableProperty(current).getValue();
}
}
current = current.getParent();
}
return Font.getDefault();
}
}
Ok thinking about it again, technically I don't need the actual structure. Just an simpler way to maybe query and call apply() on a certain subnode of the structure.
I can just implement my own helper methods of doing a more ergonomic handling for the Node::lookup method, but I thought a dedicated library for this would be a better place.
Maybe a variant of the apply() method that takes some kind of NodeSelector as an additional argument and will call the consumer with all matched nodes, e.g. by class name or id. An a sample use case, if I wanted to modify the behavior of your ValidationMarkerPane background, I could just call .apply(NodeSelector.ofStyleClass("background"), pane -> { ... })
Maybe a variant of the apply() method that takes some kind of NodeSelector as an additional argument and will call the consumer with all matched nodes, e.g. by class name or id. An a sample use case, if I wanted to modify the behavior of your ValidationMarkerPane background, I could just call .apply(NodeSelector.ofStyleClass("background"), pane -> { ... })
Hm, well, apply receives an instance of the constructed node (so it is already a ValidationMarkerPane). Any public method or field will be accessible at that point. lookup would work for sure, but perhaps nicer would be if the class itself just offer access via a normal method or field. I often just make certain sub-nodes public final if I don't care about encapsulation too much. So for example:
public class MyPane extends Region {
// accessible internal structure here:
public final Label label;
public final Button button;
// rest of class
}
And then with apply:
MyPane.of() // <-- assuming that gets you a builder, otherwise use FX.custom()
.apply(myPane -> myPane.label.setText("a different text"));
I added a snapshots now (on github, they're visible in packages).
I also now added the system that makes it possible to override builder defaults, with custom strategies. We'll see how well that works in practice. As an example, this overrides two strategies to create a new BuildContext:
The first one applies to any text, promptText, accessibleText or accessibleHelpText, allowing you localize this. These now also accepts any Object instead of just Strings (so you could do localization by calling say FX.label().text(MyEnum.ENUM_KEY) and receive that key in the strategy).
The 2nd one is what is called when you call a method like nodes(). By default, the builders already do simple stuff like filtering out nulls here and translating Strings to Labels (see the default in ChildrenStrategy) but now you can customize this fully (ie. enum keys could be turned into controls, or strings could become something other than labels, or you could treat the values you get as pairs, etc).
There is also a ContentStrategy for single nodes (ie. graphic, content of titled pane/scroll pane, etc).
To apply a BuildContext at a certain point there are a few options:
Pass it to Scenes.create()
Set it on any node in the hierarchy, using the new context() builder method
Set it temporarily (via ThreadLocal) using BuildContexts.with()
Set it globally at the start of your application (before using any builders) using BuildContexts.root()
Hm, well, apply receives an instance of the constructed node (so it is already a ValidationMarkerPane). Any public method or field will be accessible at that point. lookup would work for sure, but perhaps nicer would be if the class itself just offer access via a normal method or field. I often just make certain sub-nodes public final if I don't care about encapsulation too much. So for example:
I usually try to replace creating custom control classes with custom builder subclasses. In an ideal world, I only create builder subclasses and no subclasses of Region for example. (Unless when implementing a completely new custom control with custom skinning, but that is an exception)
I think the TextStrategy should be able to differentiate between translations keys as strings and actual string values. In my use case, I'm referencing translated strings using a string identifier (creating enums for that is just way too much work and inflexible). But for some text entries, it should not be translated and the text should be used as-is. I would say that most applications handle translations this way in some form or another.
If the strategy approach is kept, there should be at least the basic classes included for this translation approach as that will cover most of the use cases for people using the text strategy. Like two classes Translated and Fixed and easy creation methods for those.
I think the TextStrategy should be able to differentiate between translations keys as strings and actual string values. In my use case, I'm referencing translated strings using a string identifier (creating enums for that is just way too much work and inflexible). But for some text entries, it should not be translated and the text should be used as-is. I would say that most applications handle translations this way in some form or another.
If the strategy approach is kept, there should be at least the basic classes included for this translation approach as that will cover most of the use cases for people using the text strategy. Like two classes Translated and Fixed and easy creation methods for those.
That's work for the user of the framework, as they can choose their own approach that suits them. So if you want to wrap all your texts with two classes, then you can create this. Alternatively, you could go the FXML approach (where Strings starting with % are interpreted as keys) or you can think of your own unique approach (ie. lower case string = key, anything else = text).
Activity
hjohn commentedon Jan 24, 2026
The last link is broken, so not entirely sure what you are doing.
There is nothing stopping you from making a group of nodes with a single builder AFAIK; I mean the pane builders do exactly that, except the nodes are supplied. You can easily make a method that accepts a count, which then adds that number of children for example. Builders can also store information locally in fields if they want to (most however don't).
When it comes to references after building completes (ie. for the user that defines the UI), they will either have to use a variable and break from the structured style themselves, or be satisfied with things like
NodeEventHandlers orTriggers.I may be missing something though, as I can't see your last link.
crschnick commentedon Jan 24, 2026
Ah whoops, this link works: https://github.com/xpipe-io/xpipe/blob/21-release/app/src/main/java/io/xpipe/app/comp/base/LeftSplitPaneComp.java
crschnick commentedon Jan 24, 2026
Actually, I think the SplitPane one isn't the best showcase for this as the SplitPane itself has getters for the content. This is more representative of the advantages: https://github.com/xpipe-io/xpipe/blob/21-release/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java. It gives me an easy way to get a certain subnode in a built structure without having to use ids and class name lookups
crschnick commentedon Jan 24, 2026
I created a quick demo for what I thought of: crschnick@82e1855
Maybe calling it views instead of structures makes more sense
hjohn commentedon Jan 24, 2026
Can you explain what those advantages are and why you need that? I've never had the need to manipulate a sub-node directly. In my view, a sub-node is owned by its parent (or skin) and manipulating it directly in any way is a violation of encapsulation. Instead the container should selectively expose actions, properties and events, and not allow access to its internals (exposing a
Nodebasically exposes everything).I've been trying to see what you are doing here, and it seems you are creating a fancy button. The closest analogy in a code base I use is this:
This creates the titled pane you see in this image:
crschnick commentedon Jan 24, 2026
I am doing that because I extended the builder use to be more than just simple controls. It's more like compound controls that are made up of multiple individual nodes.
In theory, you could wire everything up so that everything can be manipulated directly, yes. But in practice it turned out to be much easier to just expose it via a view / structure. It's the same problem with skins in JavaFX. If you want to modify some internal thing of a skin, but it's not exposed properly, you hope that there is at least a style class associated with it to somehow look it up. Otherwise, you need an entire new skin or weird hack to just to get some small Label behave a bit differently than intended by the original skin.
In your example, let's say I want to have the title pane title be an ObservableValue. You could add that as an overloaded method or just modify the original one. After you do that, you might also want to have a graphic for the title pane label. Ok, you add a way to pass a graphic as well. Then, someone wants to change the graphic layout, etc. At some point, using views becomes much easier than to just add every possibility of customization.
crschnick commentedon Jan 30, 2026
Right now, the primary use of builders is to build individual nodes. You configure certain properties like text, graphic, etc. for a label builder and that works out fine. A structure builder primarily takes other builders as parameters that make up the larger structure.
If you want to create larger structures that are made out of many nodes/builders, the current system doesn't work that well. You have the same issue in your example, you have to create helper methods to build the node structures you want. I wanted to avoid these helper methods and instead primarily use class instances for that. Because helper methods don't scale very well when you add more and more parameters over time. And if you use custom builder class instances to replace the helper methods, you still lose the information about the individual nodes that make up the generated structure.
hjohn commentedon Jan 30, 2026
That's the primary use case for a builder. A builder that has other builders as parameters is a pane, and panes do exactly that:
There is no need to call
build.It's only a helper method because I didn't deem that pressing enough to convert to its own builder or custom control. The only reason this exists at all is because
TitledPaneis bugged and doesn't render the graphic correctly if you use a label and graphic separately (so I had to instead make my own label + graphic inside graphic of TitledPane). This issue is fixed in later FX versions as I fixed it myself.As for creating larger structures, you can do that easily. Here's a larger structure:
Note that it is encapsulated, and the outside world can create as many of these as they want, and all they have to do is link up the
publicfield. This can be converted to a builder as well, but haven't gotten around to it. Another example of a nicely encapsulated control (partially done):crschnick commentedon Jan 30, 2026
Ok thinking about it again, technically I don't need the actual structure. Just an simpler way to maybe query and call apply() on a certain subnode of the structure.
I can just implement my own helper methods of doing a more ergonomic handling for the Node::lookup method, but I thought a dedicated library for this would be a better place.
Maybe a variant of the
apply()method that takes some kind ofNodeSelectoras an additional argument and will call the consumer with all matched nodes, e.g. by class name or id. An a sample use case, if I wanted to modify the behavior of your ValidationMarkerPane background, I could just call.apply(NodeSelector.ofStyleClass("background"), pane -> { ... })hjohn commentedon Feb 1, 2026
Hm, well,
applyreceives an instance of the constructed node (so it is already aValidationMarkerPane). Any public method or field will be accessible at that point.lookupwould work for sure, but perhaps nicer would be if the class itself just offer access via a normal method or field. I often just make certain sub-nodespublic finalif I don't care about encapsulation too much. So for example:And then with apply:
Is that workable?
hjohn commentedon Feb 1, 2026
I added a snapshots now (on github, they're visible in packages).
I also now added the system that makes it possible to override builder defaults, with custom strategies. We'll see how well that works in practice. As an example, this overrides two strategies to create a new
BuildContext:The first one applies to any
text,promptText,accessibleTextoraccessibleHelpText, allowing you localize this. These now also accepts anyObjectinstead of justStrings (so you could do localization by calling sayFX.label().text(MyEnum.ENUM_KEY)and receive that key in the strategy).The 2nd one is what is called when you call a method like
nodes(). By default, the builders already do simple stuff like filtering outnulls here and translatingStrings toLabels (see the default inChildrenStrategy) but now you can customize this fully (ie. enum keys could be turned into controls, or strings could become something other than labels, or you could treat the values you get as pairs, etc).There is also a
ContentStrategyfor single nodes (ie.graphic,contentof titled pane/scroll pane, etc).To apply a
BuildContextat a certain point there are a few options:Scenes.create()context()builder methodBuildContexts.with()BuildContexts.root()It should be available in the snapshot release.
crschnick commentedon Feb 1, 2026
I usually try to replace creating custom control classes with custom builder subclasses. In an ideal world, I only create builder subclasses and no subclasses of Region for example. (Unless when implementing a completely new custom control with custom skinning, but that is an exception)
crschnick commentedon Feb 2, 2026
I think the TextStrategy should be able to differentiate between translations keys as strings and actual string values. In my use case, I'm referencing translated strings using a string identifier (creating enums for that is just way too much work and inflexible). But for some text entries, it should not be translated and the text should be used as-is. I would say that most applications handle translations this way in some form or another.
If the strategy approach is kept, there should be at least the basic classes included for this translation approach as that will cover most of the use cases for people using the text strategy. Like two classes
TranslatedandFixedand easy creation methods for those.hjohn commentedon Feb 2, 2026
That's work for the user of the framework, as they can choose their own approach that suits them. So if you want to wrap all your texts with two classes, then you can create this. Alternatively, you could go the FXML approach (where Strings starting with
%are interpreted as keys) or you can think of your own unique approach (ie. lower case string = key, anything else = text).