I've created a custom class ImageButton
to get rid of some boilerplate code
open class ImageButton(@NamedArg("image") image: Image,
@NamedArg("tooltipText") tooltipText: String,
@NamedArg("width") width: Double,
@NamedArg("height") height: Double) : Button() {
init {
prefWidth = width
minWidth = NEGATIVE_INFINITY
maxWidth = NEGATIVE_INFINITY
prefHeight = height
minHeight = NEGATIVE_INFINITY
maxHeight = NEGATIVE_INFINITY
cursor = ImageCursor.HAND
effect = ImageInput(image)
tooltip = Tooltip(tooltipText)
}
}
Now, instead of this:
<Button fx:id="deleteButton" prefWidth="32.0" prefHeight="32.0"
minHeight="-Infinity" maxHeight="-Infinity"
minWidth="-Infinity" maxWidth="-Infinity"
onMouseClicked="#deleteThePiece">
<cursor>
<ImageCursor fx:constant="HAND"/>
</cursor>
<tooltip>
<Tooltip text="Delete Current Piece"/>
</tooltip>
<effect>
<ImageInput>
<source>
<Image url="@/icons/delete.png"/>
</source>
</ImageInput>
</effect>
</Button>
I can write this:
<ImageButton fx:id="deleteButton" width="32.0" height="32.0"
onMouseClicked="#deleteThePiece" tooltipText="Delete Current Piece">
<image>
<Image url="@/icons/dice.png"/>
</image>
</ImageButton>
However, I would like to be able to shorten it even more, like this:
<ImageButton fx:id="deleteButton" width="32.0" height="32.0"
onMouseClicked="#deleteThePiece" tooltipText="Delete Current Piece">
<Image url="@/icons/dice.png"/>
</ImageButton>
I have a lot of these objects so it would be nice to be able to keep the fxml tags as short as possible.
I know that there is an annotation @DefaultProperty
that can be used to unwrap the default tag (e.g you can omit the <children>
tag inside a <Pane>
tag because it has the @DefaultProperty("children")
annotation) so I used it:
@DefaultProperty("image")
open class ImageButton(...) {...}
but then when loading fxml file I get the following error:
javafx.fxml.LoadException: Element does not define a default property.
I've done some research and come across this:
"Element does not define a default property" when @DefaultProperty is used
It, however, does not contain a solution. It only explains the problem.
So my question is:
Is it possible to use the @DefaultProperty
annotation on custom classes that use @NamedArg
annotation?
If yes, how do I achieve this?
If no, should I try constructing my ImageButton
objects differently? e.g using <fx:factory>
?
There are a few options here, but the more simple one is stated by @Slaw in a comment:
Use a String URL in the constructor instead of the Image
So this should work:
public ImageButton(@NamedArg("url") String url, @NamedArg("text") String text) {
setEffect(new ImageInput(new Image(url)));
setText(text);
}
with the FXML like:
<ImageButton fx:id="imageButton" text="Click Me!" url="@icon.png"/>
Let's explore now the use of <image />
combined with @DefaultProperty
.
ImageButton control
First of all, let's define our control. For the sake of simplicity (and also because these can't be overridden), I won't include width
and height
:
public class ImageButton extends Button {
public ImageButton(@NamedArg("image") Image image, @NamedArg("text") String text) {
setImage(image);
setText(text);
}
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image") {
@Override
protected void invalidated() {
// setGraphic(new ImageView(get()));
setEffect(new ImageInput(get()));
}
};
public final ObjectProperty<Image> imageProperty() {
return image;
}
public final Image getImage() {
return image.get();
}
public final void setImage(Image value) {
image.set(value);
}
}
And:
<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>
will work perfectly fine. However, the purpose is to remove the <image>
tag.
DefaultProperty
The theory says you could do:
@DefaultProperty("image")
public class ImageButton extends Button {
...
}
and
<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>
However, an exception is thrown:
Caused by: javafx.fxml.LoadException: Element does not define a default property.
For more details, why this exception happens, see the linked question.
Basically, as discussed within comments, @DefaultProperty
and @NamedArg
don't work together: In order to extend the FXML attributes of a given class, @NamedArg
provide new constructors to this class, which require the use of ProxyBuilder
, so FXMLLoader
will use instances of ProxyBuilder
instead, and these don't have included the @DefaultProperty
annotation.
Builders
Though builder design pattern was used in JavaFX 2.0, and it was deprecated a long time ago (in Java 8, removed in Java 9, link), there are still some builders in the current JavaFX code.
In fact, FXMLLoader
makes use of JavaFXBuilderFactory
, as default builder factory, that will call this ProxyBuilder
if NamedArg
annotations are found in the class constructor, among other builders like JavaFXImageBuilder
.
There is some description about builders here.
Builder implementation
How can we add our own builder factory? FXMLLoader
has a way: setBuilderFactory
.
Can we extend JavaFXBuilderFactory
? No, it's final, we can't extend it, we have to create one from the scratch.
ImageButtonBuilderFactory
Let's create it:
import javafx.util.Builder;
import javafx.util.BuilderFactory;
public class ImageButtonBuilderFactory implements BuilderFactory {
@Override
public Builder<?> getBuilder(Class<?> type) {
if (type == null) {
throw new NullPointerException();
}
if (type == ImageButton.class) {
return ImageButtonBuilder.create();
}
return null;
}
}
Now let's add the builder:
ImageButtonBuilder
import javafx.scene.image.Image;
import javafx.util.Builder;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Set;
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
private String text = "";
private Image image;
private ImageButtonBuilder() {}
@Override
public Set<Entry<String, Object>> entrySet() {
return new HashSet<>();
}
public static ImageButtonBuilder create() {
return new ImageButtonBuilder();
}
@Override
public Object put(String key, Object value) {
if (value != null) {
String str = value.toString();
if ("image".equals(key)) {
image = (Image) value;
} else if ("text".equals(key)) {
text = str;
} else {
throw new IllegalArgumentException("Unknown property: " + key);
}
}
return null;
}
@Override
public ImageButton build() {
return new ImageButton(image, text);
}
}
Note that ImageButton
is the same class as above (without DefaultProperty
annotation).
Using the custom builder
Now we could use our custom builder:
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("imagebutton.fxml"));
loader.setBuilderFactory(new ImageButtonBuilderFactory());
Parent root = loader.load();
Scene scene = new Scene(root);
...
where the FXML is:
<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>
</StackPane>
If we run this now, it should work. We have verified that our new builder works. If we comment out the setBuilderFactory
call, it will work as well (using NamedArg
and ProxyBuilder
). With the custom builder factory, it won't use ProxyBuilder
but our custom builder.
Final step
Finally, we can make use of DefaultProperty
to get rid of the <image>
tag.
And we'll add the annotation to the builder class, not to the control!
So now we have:
@DefaultProperty("image")
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
...
}
and finally we can remove the <image>
tag from the FXML file:
<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>
</StackPane>
and it will work!