How to properly add @DefaultProperty annotation to custom class that uses @NamedArg

pjaro :

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>?

José Pereda :

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!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=141181&siteId=1