Hey all I am needing a hand with the following:
I am trying to add the "On Action" to my custom control I create in Scene Builder 2.0.
I will have a couple of these in my scene so I am wanting to be able to have only 1 handler for all those toggle buttons. Problem being is that my custom control does not have a "On Action" section in the Code: section like other controls do?
Most built in controls look like this for their Code: section:
How do I add this function to my custom control?
My switch button code:
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; }
public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); }
public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); }
private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() {
@Override protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
@Override
public Object getBean() {
return SliderSwitch.this;
}
@Override
public String getName() {
return "onAction";
}
};
Loading it up in Scene Builder 2.0 I still do not see any action option under the Code tab.
Custom components don't automatically come with an "on action" property. You have to actually implement an onAction
property in the code1. Take a look at implementations of bulit-in controls that provide such a property for examples. Typically, the implementation of the property looks something like this:
// assumes 'this' is some subtype of 'javafx.scene.Node'
private final ObjectProperty<EventHandler<ActionEvent>> onAction =
new SimpleObjectProperty<>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
public final void setOnAction(EventHandler<ActionEvent> onAction) { this.onAction.set(onAction); }
public final EventHandler<ActionEvent> getOnAction() { return onAction.get(); }
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; }
But note that is not enough. The custom component also has to fire an ActionEvent
whenever it's appropriate. When is it appropriate? Well, that's up to the custom component.
And finally, Scene Builder unfortunately does not put the onAction
property of a custom component in the "Code" accordion. It is placed in the "Properties" accordion under a section named "Custom" at the top (see screenshot at end of example below). I'm not aware of a way to change this.
Couple of side notes:
You can actually add change listeners to properties via FXML. Though I'm not aware of a way to do that with Scene Builder.
Scene Builder 2.0 is a very outdated version2. Consider using the latest version from Gluon, which is version 22.0.0 at the time of this answer.
1. In response to a (since deleted) comment I made before posting this answer, you've updated your question to show your custom component now has an onAction
property.
2. In a comment you've pointed out that Scene Builder 2.0 does not show the "Custom" section, which means updating Scene Builder is part of the solution.
Here is an example of a custom "switch" control that provides an onAction
property. This example has the custom control actually extend Control
, which means there's also a "skin" class and a "behavior" class to keep things separate.
There is a screenshot of Scene Builder at the end of the answer.
Compiled and tested with Java 22.0.2 and JavaFX 22.0.2.
Switch.java
package com.example.control;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public class Switch extends Control {
public Switch() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public Switch(boolean selected) {
this();
setSelected(selected);
}
public void toggle() {
if (!isDisabled() && !selected.isBound()) {
setSelected(!isSelected());
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new SwitchSkin(this);
}
/* **************************************************************************
* *
* Properties *
* *
****************************************************************************/
// -- selected property
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected") {
private boolean wasSelected;
@Override
protected void invalidated() {
boolean isSelected = get();
if (wasSelected != isSelected) {
pseudoClassStateChanged(SELECTED, isSelected);
fireEvent(new ActionEvent());
wasSelected = isSelected;
}
}
};
public final void setSelected(boolean selected) {
this.selected.set(selected);
}
public final boolean isSelected() {
return selected.get();
}
public final BooleanProperty selectedProperty() {
return selected;
}
// -- onAction property
private ObjectProperty<EventHandler<? super ActionEvent>> onAction;
public final void setOnAction(EventHandler<? super ActionEvent> onAction) {
if (this.onAction != null || onAction != null) {
onActionProperty().set(onAction);
}
}
public final EventHandler<? super ActionEvent> getOnAction() {
return onAction == null ? null : onAction.get();
}
public final ObjectProperty<EventHandler<? super ActionEvent>> onActionProperty() {
if (onAction == null) {
onAction = new SimpleObjectProperty<>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
}
return onAction;
}
/* **************************************************************************
* *
* CSS *
* *
****************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "switch";
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
}
SwitchSkin.java
package com.example.control;
import javafx.animation.Animation;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
class SwitchSkin extends SkinBase<Switch> {
private static final Duration ANIMATION_DURATION = Duration.millis(100);
private final Circle thumb = new Circle(10);
private final ParallelTransition animation;
private final TranslateTransition translateAnimation;
private SwitchBehavior behavior;
SwitchSkin(Switch control) {
super(control);
var fillAnimation = new FillTransition(ANIMATION_DURATION);
fillAnimation.setFromValue(Color.FIREBRICK);
fillAnimation.setToValue(Color.FORESTGREEN);
thumb.setFill(fillAnimation.getFromValue());
translateAnimation = new TranslateTransition(ANIMATION_DURATION);
translateAnimation.setFromX(0);
animation = new ParallelTransition(thumb, fillAnimation, translateAnimation);
}
@Override
public void install() {
var control = getSkinnable();
var bgFill = new BackgroundFill(Color.GRAY, new CornerRadii(10), new Insets(2));
control.setBackground(new Background(bgFill));
control.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
control.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
getChildren().add(thumb);
registerChangeListener(control.selectedProperty(), _ -> selectedChanged());
behavior = new SwitchBehavior(control);
}
@Override
public void dispose() {
super.dispose();
if (behavior != null) {
behavior.dispose();
behavior = null;
}
}
private void selectedChanged() {
animation.setRate(isSelected() ? 1 : -1);
animation.play();
}
private boolean isSelected() {
return getSkinnable().isSelected();
}
private boolean animationNotRunning() {
return animation.getStatus() != Animation.Status.RUNNING;
}
@Override
protected void layoutChildren(
double contentX, double contentY, double contentWidth, double contentHeight) {
positionInArea(
thumb, contentX, contentY, contentWidth, contentHeight, -1, HPos.LEFT, VPos.CENTER);
double toX = contentX + contentWidth - thumb.getLayoutBounds().getWidth();
translateAnimation.setToX(toX);
if (isSelected() && animationNotRunning() && thumb.getTranslateX() != toX) {
animation.setRate(1);
animation.playFromStart();
} else if (!isSelected() && animationNotRunning() && thumb.getTranslateX() != 0) {
animation.setRate(-1);
animation.playFrom(ANIMATION_DURATION);
}
}
@Override
protected double computePrefWidth(
double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return leftInset + rightInset + (thumb.getRadius() * 4);
}
@Override
protected double computePrefHeight(
double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + bottomInset + (thumb.getRadius() * 2);
}
}
SwitchBehavior.java
package com.example.control;
import java.util.Objects;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
class SwitchBehavior {
private final EventHandler<MouseEvent> onClick = this::handleMouseClicked;
private final WeakEventHandler<MouseEvent> weakOnClick = new WeakEventHandler<>(onClick);
private final Switch node;
SwitchBehavior(Switch node) {
this.node = Objects.requireNonNull(node);
node.addEventHandler(MouseEvent.MOUSE_CLICKED, weakOnClick);
}
private void handleMouseClicked(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
node.toggle();
}
}
void dispose() {
node.removeEventHandler(MouseEvent.MOUSE_CLICKED, weakOnClick);
}
}
Using Scene Builder 22.0.0.