SplitItemDelegate.java

package swingtree;

import sprouts.Action;
import swingtree.components.JSplitButton;

import javax.swing.JMenuItem;
import java.awt.event.ActionEvent;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Instances of this are delegated to the individual {@link JSplitButton} items
 * and their {@link ActionEvent}s exposed to you inside your {@link Action} handlers,
 * like for example one you would supply to {@link SplitItem#onSelection(Action)}.
 * This class exists to give said actions all the necessary context
 * they need to perform their tasks.
 *
 * @param <I> The {@link JMenuItem} subtype for which this context was created.
 */
public final class SplitItemDelegate<I extends JMenuItem> extends AbstractDelegate<I> {
    
    private final ActionEvent       _event;
    private final JSplitButton      _splitButton;
    private final Supplier<List<I>> _siblingsSource;


    SplitItemDelegate(
        ActionEvent       event,
        JSplitButton      splitButton,
        Supplier<List<I>> siblingsSource,
        I                 currentItem
    ) {
        super(true, currentItem, splitButton);
        Objects.requireNonNull(event);
        Objects.requireNonNull(splitButton);
        Objects.requireNonNull(siblingsSource);
        _event          = event;
        _splitButton    = splitButton;
        _siblingsSource = () -> Collections.unmodifiableList(siblingsSource.get());
    }

    /**
     *  Exposes the underlying {@link ActionEvent} that is being handled by this delegate.
     *  It is a semantic event which indicates that a component-defined action occurred.
     *  This high-level event is generated by the split button when
     *  the component-specific action occurs (such as being pressed).
     *  The event is originally passed to every {@code ActionListener} object
     *  that registered to receive such events using the component's
     *  {@code addActionListener} method.
     *
     * @return The {@link ActionEvent} which caused this action to be executed.
     */
    public ActionEvent getEvent() {
        return _event;
    }

    /**
     *  Returns the underlying {@link JSplitButton} instance or throws an exception if
     *  the current thread is not the Swing thread.
     *
     * @return The {@link JSplitButton} to which this {@link SplitItem} (and its {@link JMenuItem}) belongs.
     */
    public JSplitButton getSplitButton() {
        // We make sure that only the Swing thread can access the component:
        if ( UI.thisIsUIThread() )
            return _splitButton;
        else
            throw new IllegalStateException(
                    "Split button can only be accessed by the Swing thread."
                );
    }

    /**
     *  Exposes the {@link JMenuItem} which is currently selected by the user
     *  and displayed as the button item of the {@link JSplitButton}.
     *  This is the item which caused the {@link ActionEvent} to be fired.
     *
     * @return The {@link JMenuItem} which caused this action to be executed.
     */
    public final I getCurrentItem() {
        // We make sure that only the Swing thread can access the component:
        if ( UI.thisIsUIThread() )
            return _component();
        else
            throw new IllegalStateException(
                    "The current button item can only be accessed by the Swing thread."
                );
    }

    /**
     *  This method provides a convenient way to access all the children of the parent component
     *  of the split item this delegate is for.
     *
     * @return A list of all the {@link JMenuItem} which constitute the options exposed by the {@link JSplitButton}.
     */
    public List<I> getSiblinghood() {
        // We make sure that only the Swing thread can access the sibling components:
        if ( UI.thisIsUIThread() )
            return _siblingsSource.get();
        else
            throw new IllegalStateException(
                    "Sibling components can only be accessed by the Swing thread."
                );
    }

    /**
     *  Exposes the "siblings" of the delegated component, which refers
     *  to all children of its parent component, except for itself.
     *  This is contrary to the {@link #getSiblinghood()} method which returns all children of the parent component
     *  including the current component.
     *
     * @return A list of all the {@link JMenuItem} which constitute the options exposed by the {@link JSplitButton}
     * except the current {@link JMenuItem} exposed by {@link #getCurrentItem()}.
     */
    public List<I> getSiblings() {
        // We make sure that only the Swing thread can access the sibling components:
        if (!UI.thisIsUIThread())
            throw new IllegalStateException(
                    "Sibling components can only be accessed by the Swing thread."
            );
        return _siblingsSource.get()
                .stream()
                .filter(s -> s != getCurrentItem())
                .collect(Collectors.toList());
    }

    /**
     * Selects the current {@link JMenuItem} by passing {@code true}
     * to the {@link JMenuItem#setSelected(boolean)} method.
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> selectCurrentItem() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::selectCurrentItem);
            return this;
        }
        this.getCurrentItem().setSelected(true);
        return this;
    }

    /**
     * Selects only the current {@link JMenuItem} by passing {@code true}
     * to the {@link JMenuItem#setSelected(boolean)} method.
     * All other {@link JMenuItem}s will be unselected.
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> selectOnlyCurrentItem() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::selectOnlyCurrentItem);
            return this;
        }
        this.unselectAllItems();
        this.getCurrentItem().setSelected(true);
        return this;
    }

    /**
     * Unselects the current {@link JMenuItem} by passing {@code false}
     * to the {@link JMenuItem#setSelected(boolean)} method.
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> unselectCurrentItem() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::unselectCurrentItem);
            return this;
        }
        this.getCurrentItem().setSelected(false);
        return this;
    }

    /**
     * Unselects all {@link JMenuItem}s by passing {@code false}
     * to their {@link JMenuItem#setSelected(boolean)} methods.
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> unselectAllItems() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::unselectAllItems);
            return this;
        }
        getSiblinghood().forEach(it -> it.setSelected(false));
        return this;
    }

    /**
     * Selects all {@link JMenuItem}s by passing {@code true}
     * to their {@link JMenuItem#setSelected(boolean)} methods.
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> selectAllItems() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::selectAllItems);
            return this;
        }
        getSiblinghood().forEach(it -> it.setSelected(true));
        return this;
    }

    /**
     * Use this to conveniently make the {@link JSplitButton} display the text
     * of the currently selected {@link JMenuItem} (button item).
     *
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> displayCurrentItemText() {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(this::displayCurrentItemText);
            return this;
        }
        if (getCurrentItem() != null)
            getSplitButton().setText(getCurrentItem().getText());
        return this;
    }

    /**
     *  Takes the supplied {@link String} and passes it to the
     *  {@link JSplitButton#setText(String)} while running on the UI thread.
     *
     * @param text The text which should be displayed on the {@link JSplitButton}.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> setButtonText( String text ) {
        // We make sure that only the Swing thread can modify components:
        if ( !UI.thisIsUIThread() ) {
            UI.run(() -> setButtonText(text));
            return this;
        }
        NullUtil.nullArgCheck(text, "text", String.class);
        _splitButton.setText(text);
        return this;
    }

    /**
     *  Allows you to retrieve the text currently displayed on the {@link JSplitButton}.
     *
     * @return The text displayed on the {@link JSplitButton}.
     */
    public String getButtonText() {
        return _splitButton.getText();
    }

    /**
     *  Appends the supplied {@link String} to the text displayed on the {@link JSplitButton}.
     * @param postfix The text which should be appended to the text displayed on the {@link JSplitButton}.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> appendToButtonText( String postfix ) {
        NullUtil.nullArgCheck(postfix, "postfix", String.class);
        // We make sure that only the Swing thread can modify components:
        if ( !UI.thisIsUIThread() ) {
            UI.run(() -> appendToButtonText(postfix));
            return this;
        }
        _splitButton.setText(this.getButtonText() + postfix);
        return this;
    }

    /**
     *  A convenience method which takes the supplied {@link String} and prepends it to the
     *  text displayed on the {@link JSplitButton}.
     *  This method is equivalent to calling {@link #setButtonText(String)} with the concatenation of the
     *  supplied {@link String} and the current text displayed on the {@link JSplitButton}.
     *
     * @param prefix The text which should be prepended to the text displayed on the {@link JSplitButton}.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> prependToButtonText( String prefix ) {
        NullUtil.nullArgCheck(prefix, "postfix", String.class);
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(() -> prependToButtonText(prefix));
            return this;
        }
        _splitButton.setText(prefix + this.getButtonText());
        return this;
    }

    /**
     * Selects the targeted split item ({@link JMenuItem}).
     *
     * @param i The item index of the {@link JMenuItem} which should be selected.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> selectItem(int i) {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(() -> selectItem(i));
            return this;
        }
        getSiblinghood().get(i).setSelected(true);
        return this;
    }

    /**
     * Selects the targeted split item ({@link JMenuItem}) and unselects all other items.
     *
     * @param i The item index of the {@link JMenuItem} which should be selected exclusively.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> selectOnlyItem(int i) {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(() -> selectOnlyItem(i));
            return this;
        }
        unselectAllItems().getSiblinghood().get(i).setSelected(true);
        return this;
    }

    /**
     * Unselects the targeted split item ({@link JMenuItem}).
     *
     * @param i The item index of the {@link JMenuItem} which should be unselected.
     * @return This {@link SplitItemDelegate} instance to allow for method chaining.
     */
    public SplitItemDelegate<I> unselectItem(int i) {
        // We make sure that only the Swing thread can modify components:
        if (!UI.thisIsUIThread()) {
            UI.run(() -> unselectItem(i));
            return this;
        }
        getSiblinghood().get(i).setSelected(false);
        return this;
    }

}