javajavafxjavafx-8openide

JavaFX Toggle MenuButton


Is there a JavaFX MenuButton (specifically a SplitMenuButton) that also allows for toggling its selected state? The Swing equivalent would be the OpenIDE JToggleButton that you can create as:

JToggleButton button = DropDownButtonFactory.createDropDownToggleButton(icon, menu)

So, when the user clicks on the action-area, the button's selected state should toggle in addition to firing whatever is associated with the ButtonBase.onAction property. Clicking the arrow should show the drop-down menu as expected.


Solution

  • Ok, I think I've managed to figure it out. You need to create a new class called ToggleSplitMenuButton that extends from SplitMenuButton:

    import java.net.URL;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.BooleanPropertyBase;
    import javafx.collections.ObservableList;
    import javafx.css.PseudoClass;
    import javafx.event.ActionEvent;
    import javafx.scene.AccessibleAttribute;
    import javafx.scene.control.MenuItem;
    import javafx.scene.control.SplitMenuButton;
    
    public class ToggleSplitMenuButton extends SplitMenuButton {
    
        private static final String STYLESHEET = "toggle-split-menu-button.css";
    
        /***************************************************************************
         *                                                                         *
         * Constructors                                                            *
         *                                                                         *
         **************************************************************************/
    
        public ToggleSplitMenuButton() {
            super();
            initialize();
        }
    
        public ToggleSplitMenuButton(MenuItem ... items) {
            super(items);
            initialize();
        }
    
        private void initialize() {
            Class<?> clazz = getClass();
            URL resource = clazz.getResource(STYLESHEET);
            String stylesheet = resource.toExternalForm();
            ObservableList<String> stylesheets = getStylesheets();
            stylesheets.add(stylesheet);
        }
    
        /***************************************************************************
         *                                                                         *
         * Properties                                                              *
         *                                                                         *
         **************************************************************************/
        /**
         * Indicates whether this toggle split menu button is selected. This can be
         * manipulated programmatically.
         */
        private BooleanProperty selected;
    
        public final void setSelected(boolean value) {
            selectedProperty().set(value);
        }
    
        public final boolean isSelected() {
            return selected == null ? false : selected.get();
        }
    
        public final BooleanProperty selectedProperty() {
            if (selected == null) {
                selected = new BooleanPropertyBase() {
    
                    @Override
                    protected void invalidated() {
                        final boolean selected = get();
                        pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, selected);
                        notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED);
                    }
    
                    @Override
                    public Object getBean() {
                        return ToggleSplitMenuButton.this;
                    }
    
                    @Override
                    public String getName() {
                        return "selected";
                    }
                };
            }
            return selected;
        }
    
        /***************************************************************************
         *                                                                         *
         * Methods                                                                 *
         *                                                                         *
         **************************************************************************/
    
        @Override
        public void fire() {
            if (!isDisabled()) {
                setSelected(!isSelected());
                fireEvent(new ActionEvent());
            }
        }
    
        /***************************************************************************
         *                                                                         *
         * Stylesheet Handling                                                     *
         *                                                                         *
         **************************************************************************/
    
        private static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected");
    }
    

    You also need to create a corresponding stylesheet (called toggle-split-menu-button.css) to style the component correctly:

    .split-menu-button:selected > .label {
        -fx-background-color:
            -fx-shadow-highlight-color,
            linear-gradient(to bottom, derive(-fx-outer-border, -20%), -fx-outer-border),
            linear-gradient(to bottom,
                    derive(-fx-color, -22%) 0%,
                    derive(-fx-color, -13%) 20%,
                    derive(-fx-color, -11%) 50%);
    }
    .split-menu-button:selected > .arrow-button {
        -fx-background-color:
            -fx-shadow-highlight-color,
            linear-gradient(to bottom, derive(-fx-outer-border, -20%), -fx-outer-border),
            linear-gradient(to bottom,
                    derive(-fx-color, -22%) 0%,
                    derive(-fx-color, -13%) 20%,
                    derive(-fx-color, -11%) 50%);
    }
    .split-menu-button:selected:focused > .label {
        -fx-background-color:
            -fx-shadow-highlight-color,
            linear-gradient(to bottom,
                derive(-fx-color, -22%) 0%,
                derive(-fx-color, -13%) 20%,
                derive(-fx-color, -11%) 50%),
            -fx-faint-focus-color,
            linear-gradient(to bottom,
                derive(-fx-color, -22%) 0%,
                derive(-fx-color, -13%) 20%,
                derive(-fx-color, -11%) 50%);
    }
    .split-menu-button:selected:focused > .arrow-button {
        -fx-background-color:
            -fx-shadow-highlight-color,
            linear-gradient(to bottom,
                derive(-fx-color, -22%) 0%,
                derive(-fx-color, -13%) 20%,
                derive(-fx-color, -11%) 50%),
            -fx-faint-focus-color,
            linear-gradient(to bottom,
                derive(-fx-color, -22%) 0%,
                derive(-fx-color, -13%) 20%,
                derive(-fx-color, -11%) 50%);
    }