SplitItem.java

package swingtree;


import org.jspecify.annotations.Nullable;
import sprouts.Action;
import sprouts.Val;
import sprouts.Var;
import swingtree.components.JSplitButton;

import javax.swing.*;
import java.util.Objects;
import java.util.Optional;

/**
 *  An immutable data carrier exposing everything needed to configure an item of a {@link JSplitButton}.
 *  {@link SplitItem}s will be turned into button options for the {@link JSplitButton}
 *  which can be supplied to a split button through the builder API exposed by {@link UIForSplitButton} like so:
 *  <pre>{@code
 *      UI.splitButton("Hey!")
 *      .add(UI.splitItem("first"))
 *      .add(UI.splitItem("second").onClick( it -> ... ))
 *      .add(UI.splitItem("third").onClick( it -> ... ).onSelected( it -> ... ))
 *  }</pre>
 * 	<p>
 * 	<b>For more information, please take a look at the
 * 	<a href="https://globaltcad.github.io/swing-tree/">living swing-tree documentation</a>
 * 	where you can browse a large collection of examples demonstrating how to use the API of this class.</b>
 *
 * @param <I> The type of the item which will be passed to the {@link Action}s.
 */
public final class SplitItem<I extends JMenuItem>
{
    /**
     *  A factory method to create a {@link SplitItem} for a {@link JSplitButton} from a simple text string
     *  which will be displayed on the {@link SplitItem} when it is part of a clicked {@link JSplitButton}.
     *
     * @param text The text which should be displayed on the {@link SplitItem} (and its underlying {@link JMenuItem}).
     * @return A {@link SplitItem} wrapping a simple {@link JMenuItem} displaying the provided text.
     */
    public static SplitItem<JMenuItem> of( String text ) {
        NullUtil.nullArgCheck(text, "text", String.class);
        return new SplitItem<>(new JMenuItem(text));
    }

    /**
     *  A factory method to create a {@link SplitItem} for a {@link JSplitButton} from a {@link Val} property
     *  which will be used to dynamically determine the text displayed on the {@link SplitItem}.
     *
     * @param text The text property whose text should be displayed on the {@link SplitItem} (and its underlying {@link JMenuItem}).
     *             When the text of the property changes, then the text of the {@link JMenuItem} will be updated accordingly.
     * @return A {@link SplitItem} wrapping a simple {@link JMenuItem} displaying the provided text.
     */
    public static SplitItem<JMenuItem> of( Val<String> text ) {
        NullUtil.nullArgCheck(text, "text", Val.class);
        return new SplitItem<>(UI.of(new JMenuItem()).withText(text).getComponent());
    }

    /**
     *  A factory method to create a {@link SplitItem} for a {@link JSplitButton} from a {@link JMenuItem} subtype.
     *
     * @param item The {@link JMenuItem} subtype for which a {@link SplitItem} (for {@link JSplitButton}) should be created.
     * @return A {@link SplitItem} wrapping the provided {@link JMenuItem} type.
     * @param <I> The type parameter for the provided item type.
     */
    public static <I extends JMenuItem> SplitItem<I> of( I item ) {
        NullUtil.nullArgCheck(item, "item", JMenuItem.class);
        return new SplitItem<>(item);
    }

    /**
     *  Use this to create a {@link SplitItem} for a {@link JSplitButton} from a {@link UIForMenuItem}
     *  UI declaration.
     *  This is useful when you want to create a {@link SplitItem} from a {@link JMenuItem} which is configured
     *  using the declarative UI builder API exposed by {@link UIForMenuItem}.
     *  See {@link UI#menuItem()} for more information.
     *
     * @param item The {@link UIForMenuItem} which wraps a {@link  JMenuItem} for which a {@link SplitItem} should be created.
     * @param <M> The type parameter for the provided item type, a subtype of {@link JMenuItem}.
     * @return A {@link SplitItem} wrapping {@link JMenuItem} represented by the provided UI builder.
     */
    public static <M extends JMenuItem> SplitItem<M> of( UIForMenuItem<M> item ) {
        NullUtil.nullArgCheck(item, "item", UIForMenuItem.class);
        return new SplitItem<>(item.getComponent());
    }

    private final I _item;
    private final @Nullable Action<SplitItemDelegate<I>> _onButtonClick;
    private final @Nullable Action<SplitItemDelegate<I>> _onItemSelected;
    private final @Nullable Val<Boolean> _isEnabled;


    private SplitItem( I item ) {
        _item           = Objects.requireNonNull(item);
        _onButtonClick  = null;
        _onItemSelected = null;
        _isEnabled      = null;
    }

    private SplitItem(
        I item,
        @Nullable Action<SplitItemDelegate<I>> onClick,
        @Nullable Action<SplitItemDelegate<I>> onSelected,
        @Nullable Val<Boolean> isEnabled
    ) {
        _item           = Objects.requireNonNull(item);
        _onButtonClick  = onClick;
        _onItemSelected = onSelected;
        _isEnabled      = isEnabled;
    }

    /**
     *  Sets the {@link JMenuItem#setSelected(boolean)} flag of the underlying {@link JMenuItem} to {@code true}.
     *
     * @return An immutable copy of this with the provided text set.
     */
    public SplitItem<I> makeSelected() {
        _item.setSelected(true);
        return this;
    }

    /**
     *  Use this to register an action which will be called when the {@link JSplitButton}
     *  is being pressed and this {@link SplitItem} was selected to be the primary button.
     *  <pre>{@code
     *      UI.splitButton("Hey!")
     *      .add(UI.splitItem("load"))
     *      .add(UI.splitItem("save").onClick( it -> doSaving() ))
     *      .add(UI.splitItem("delete"))
     *  }</pre>
     *
     * @param action The action lambda which will be called when the {@link JSplitButton} is being pressed
     *               and this {@link SplitItem} was selected.
     * @return An immutable copy of this with the provided lambda set.
     */
    public SplitItem<I> onButtonClick( Action<SplitItemDelegate<I>> action ) {
        NullUtil.nullArgCheck(action, "action", Action.class);
        if ( _onButtonClick != null )
            throw new IllegalArgumentException("Property already specified!");
        return new SplitItem<>(_item, action, _onItemSelected, _isEnabled);
    }

    /**
     *  Use this to perform some action when the user selects a {@link SplitItem} among all other
     *  split button items.
     *  A common use case would be to set the text of the {@link JSplitButton} by calling
     *  the {@link SplitItemDelegate#getSplitButton()} method on the context object supplied to the
     *  provided action lambda like so:
     *  <pre>{@code
     *      UI.splitButton("Hey!")
     *      .add(UI.splitItem("first"))
     *      .add(UI.splitItem("second").onSelected( it -> it.getSplitButton().setText("Hey hey!") ))
     *      .add(UI.splitItem("third"))
     *  }</pre>
     *
     * @param action The action which will be called when the button was selected and which will
     *               receive some context information in the form of a {@link SplitItemDelegate} instance.
     * @return An immutable copy of this with the provided lambda set.
     */
    public SplitItem<I> onSelection( Action<SplitItemDelegate<I>> action ) {
        NullUtil.nullArgCheck(action, "action", Action.class);
        if ( _onItemSelected != null ) throw new IllegalArgumentException("Property already specified!");
        return new SplitItem<>(_item, _onButtonClick, action, _isEnabled);
    }

    /**
     *  Dynamically determines whether this {@link SplitItem} is enabled or not based on the value of the provided
     *  observable boolean property. This means that whenever the value of the property changes, the enabled state
     *  of this {@link SplitItem} will change accordingly. This is done by calling {@link JMenuItem#setEnabled(boolean)}
     *  on the underlying {@link JMenuItem} with the value of the property.
     *
     * @param isEnabled An observable boolean property which will dynamically determine whether this {@link SplitItem}
     *                  is enabled or not. So when the property changes, the enabled state of this {@link SplitItem}
     *                  will be updated accordingly.
     * @return An immutable copy of this with the provided property set.
     */
    public SplitItem<I> isEnabledIf( Var<Boolean> isEnabled ) {
        NullUtil.nullArgCheck(isEnabled, "isEnabled", Var.class);
        if ( _isEnabled != null ) throw new IllegalArgumentException("Property already specified!");
        return new SplitItem<>(_item, _onButtonClick, _onItemSelected, isEnabled);
    }

    I getItem() { return _item; }

    Action<SplitItemDelegate<I>> getOnClick() { return _onButtonClick == null ? it -> {} : _onButtonClick; }

    Action<SplitItemDelegate<I>> getOnSelected() { return _onItemSelected == null ? c -> {} : _onItemSelected; }

    Optional<Val<Boolean>> getIsEnabled() { return Optional.ofNullable(_isEnabled); }

}