UIForSplitButton.java
package swingtree;
import org.slf4j.Logger;
import sprouts.Action;
import sprouts.Event;
import sprouts.From;
import sprouts.Var;
import swingtree.components.JSplitButton;
import swingtree.style.ComponentExtension;
import javax.swing.AbstractButton;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.event.ActionEvent;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* A SwingTree builder node designed for configuring {@link JSplitButton} instances.
*/
public final class UIForSplitButton<B extends JSplitButton> extends UIForAnyButton<UIForSplitButton<B>, B>
{
private static Logger log = org.slf4j.LoggerFactory.getLogger(UIForSplitButton.class);
private final BuilderState<B> _state;
/**
* Creates a new instance wrapping the given {@link JSplitButton} component.
*
* @param state The {@link BuilderState} modelling how the component is built.
*/
UIForSplitButton( BuilderState<B> state ) {
Objects.requireNonNull(state);
_state = state.withMutator(this::_initialize);
}
private void _initialize( B thisComponent ) {
ExtraState.of( thisComponent, extraState -> {
thisComponent.setPopupMenu(extraState.popupMenu);
thisComponent.addButtonClickedActionListener(e -> _runInApp(()->{
List<JMenuItem> selected = _getSelected(thisComponent);
for ( JMenuItem item : selected ) {
Action<SplitItemDelegate<JMenuItem>> action = extraState.options.get(item);
if ( action != null )
action.accept(
new SplitItemDelegate<>(
e,
thisComponent,
()-> new ArrayList<>(extraState.options.keySet()),
item
)
);
}
}));
});
}
@Override
protected BuilderState<B> _state() {
return _state;
}
@Override
protected UIForSplitButton<B> _newBuilderWithState(BuilderState<B> newState ) {
return new UIForSplitButton<>(newState);
}
private List<JMenuItem> _getSelected(B component) {
ExtraState state = ExtraState.of(component);
return Arrays.stream(state.popupMenu.getComponents())
.filter( c -> c instanceof JMenuItem )
.map( c -> (JMenuItem) c )
.filter(AbstractButton::isSelected)
.collect(Collectors.toList());
}
/**
* Use this to build {@link JSplitButton}s where the selectable options
* are represented by an {@link Enum} type, and the click event is
* handles by an {@link Event} instance.
*
* @param selection The {@link Var} which holds the currently selected {@link Enum} value.
* This will be updated when the user selects a new value.
* @param clickEvent The {@link sprouts.Event} which will be fired when the user clicks on the button.
* @return A UI builder instance wrapping a {@link JSplitButton}.
* @param <E> The {@link Enum} type defining the selectable options.
*/
public <E extends Enum<E>> UIForSplitButton<B> withSelection( Var<E> selection, Event clickEvent ) {
NullUtil.nullArgCheck(selection, "selection", Var.class);
NullUtil.nullArgCheck(clickEvent, "clickEvent", Event.class);
return withText(selection.viewAsString())
._with( thisComponent -> {
for ( E e : selection.type().getEnumConstants() )
_addSplitItem(
UI.splitItem(e.toString())
.onButtonClick( it -> clickEvent.fire() )
.onSelection( it -> {
it.selectOnlyCurrentItem();
it.setButtonText(e.toString());
selection.set(From.VIEW, e);
}),
thisComponent
);
})
._this();
}
/**
* Use this to build {@link JSplitButton}s where the selectable options
* are represented by an {@link Enum} type.
*
* @param selection The {@link Var} which holds the currently selected {@link Enum} value.
* This will be updated when the user selects a new value.
* @param <E> The {@link Enum} type defining the selectable options.
* @return A UI builder instance wrapping a {@link JSplitButton}.
*/
public <E extends Enum<E>> UIForSplitButton<B> withSelection( Var<E> selection ) {
NullUtil.nullArgCheck(selection, "selection", Var.class);
return withText(selection.viewAsString())
._with( thisComponent -> {
for ( E e : selection.type().getEnumConstants() )
_addSplitItem(
UI.splitItem(e.toString())
.onSelection( it -> {
it.selectOnlyCurrentItem();
it.setButtonText(e.toString());
selection.set(From.VIEW, e);
}),
thisComponent
);
})
._this();
}
/**
* {@link Action}s registered here will be called when the split part of the
* {@link JSplitButton} was clicked.
* The provided lambda receives a delegate object with a rich API
* exposing a lot of context information including not
* only the current {@link JSplitButton} instance, but also
* the currently selected {@link JMenuItem} and a list of
* all other items.
*
* @param action The {@link Action} which will receive an {@link ComponentDelegate}
* exposing all essential components making up this {@link JSplitButton}.
* @return This very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> onSplitClick(
Action<SplitButtonDelegate<JMenuItem>> action
) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
ExtraState state = ExtraState.of(thisComponent);
thisComponent.addSplitButtonClickedActionListener(
e -> _runInApp(()->action.accept(
new SplitButtonDelegate<>(
thisComponent,
new SplitItemDelegate<>(
e,
thisComponent,
() -> new ArrayList<>(state.options.keySet()),
state.lastSelected[0]
)
)
)
)
);
})
._this();
}
/**
* {@link Action}s registered here will be called when the
* user selects a {@link JMenuItem} from the popup menu
* of this {@link JSplitButton}.
* The delegate passed to the provided action
* lambda exposes a lot of context information including not
* only the current {@link JSplitButton} instance, but also
* the currently selected {@link JMenuItem} and a list of
* all other items.
*
* @param action The {@link Action} which will receive an {@link SplitItemDelegate}
* exposing all essential components making up this {@link JSplitButton}.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if the provided action is null.
*/
public UIForSplitButton<B> onSelection(
Action<SplitButtonDelegate<JMenuItem>> action
) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
ExtraState state = ExtraState.of(thisComponent);
state.onSelections.add(action);
})
._this();
}
/**
* Use this as an alternative to {@link #onClick(Action)} to register
* a button click action with an action lambda having
* access to a delegate with more context information including not
* only the current {@link JSplitButton} instance, but also
* the currently selected {@link JMenuItem} and a list of
* all other items.
*
* @param action The {@link Action} which will receive an {@link ComponentDelegate}
* exposing all essential components making up this {@link JSplitButton}.
* @return This very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> onButtonClick(
Action<SplitItemDelegate<JMenuItem>> action
) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
ExtraState state = ExtraState.of(thisComponent);
thisComponent.addButtonClickedActionListener(
e -> _runInApp(()->action.accept(
new SplitItemDelegate<>(
e,
thisComponent,
() -> new ArrayList<>(state.options.keySet()),
state.lastSelected[0]
)
))
);
})
._this();
}
/**
* Use this to register a basic action for when the
* {@link JSplitButton} button is being clicked (not the split part).
* If you need more context information delegated to the action
* then consider using {@link #onButtonClick(Action)}.
*
* @param action An {@link Action} instance which will be wrapped by an {@link ComponentDelegate} and passed to the button component.
* @return This very instance, which enables builder-style method chaining.
*/
@Override
public UIForSplitButton<B> onClick( Action<ComponentDelegate<B, ActionEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent ->
thisComponent.addButtonClickedActionListener(
e -> _runInApp(()->action.accept(
new ComponentDelegate<>( thisComponent, e )
))
)
)
._this();
}
/**
* Registers a listener to be notified when the split button is opened,
* meaning its popup menu is shown after the user clicks on the split button drop
* down button.
*
* @param action the action to be executed when the split button is opened.
* @return this very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> onOpen( Action<ComponentDelegate<B, PopupMenuEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupOpen(thisComponent, e ->
_runInApp(()->action.accept(new ComponentDelegate<>( thisComponent, e )) )
);
})
._this();
}
private void _onPopupOpen( B thisComponent, Consumer<PopupMenuEvent> consumer ) {
JPopupMenu popupMenu = thisComponent.getPopupMenu();
if ( popupMenu == null )
return;
popupMenu.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
// This method is called before the popup menu becomes visible.
consumer.accept(e);
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {/* Not relevant here */}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {/* Not relevant here */}
});
}
/**
* Registers a listener to be notified when the split button is closed,
* meaning its popup menu is hidden after the user clicks on the split button drop
* down button.
*
* @param action the action to be executed when the split button is closed.
* @return this very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> onClose( Action<ComponentDelegate<B, PopupMenuEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupClose(thisComponent,
e -> _runInApp(()->action.accept(new ComponentDelegate<>( thisComponent, e )) )
);
})
._this();
}
private void _onPopupClose( B thisComponent, Consumer<PopupMenuEvent> consumer ) {
JPopupMenu popupMenu = thisComponent.getPopupMenu();
if ( popupMenu == null )
return;
popupMenu.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {/* Not relevant here */}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// This method is called before the popup menu becomes invisible.
consumer.accept(e);
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {/* Not relevant here */}
});
}
/**
* Registers a listener to be notified when the split button options drop down popup is canceled,
* which typically happens when the user clicks outside the popup menu.
*
* @param action the action to be executed when the split button popup is canceled.
* @return this very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> onCancel( Action<ComponentDelegate<B, PopupMenuEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupCancel(thisComponent,
e -> _runInApp(()->action.accept(new ComponentDelegate<>( thisComponent, e )) )
);
})
._this();
}
private void _onPopupCancel( B thisComponent, Consumer<PopupMenuEvent> consumer ) {
JPopupMenu popupMenu = thisComponent.getPopupMenu();
if ( popupMenu == null )
return;
popupMenu.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {/* Not relevant here */}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {/* Not relevant here */}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
// This method is called when the popup menu is canceled.
consumer.accept(e);
}
});
}
/**
* Use this to add a {@link JMenuItem} to the {@link JSplitButton} popup menu.
*
* @param forItem The builder whose wrapped {@link JMenuItem} will be added to and exposed
* by the {@link JSplitButton} once the split part was pressed.
* @param <M> The type of the {@link JMenuItem} wrapped by the given {@link UIForMenuItem} instance.
* @return This very instance, which enables builder-style method chaining.
*/
public <M extends JMenuItem> UIForSplitButton<B> add( UIForMenuItem<M> forItem ) {
NullUtil.nullArgCheck(forItem, "forItem", UIForMenuItem.class);
return this.add(forItem.getComponent());
}
/**
* Use this to add a {@link JMenuItem} to the {@link JSplitButton} popup menu.
* @param item A {@link JMenuItem} which will be exposed by this {@link JSplitButton} once the split part was pressed.
* @return This very instance, which enables builder-style method chaining.
*/
public UIForSplitButton<B> add( JMenuItem item ) {
NullUtil.nullArgCheck(item, "item", JMenuItem.class);
return this.add(SplitItem.of(item));
}
/**
* Use this to add a {@link SplitItem} to the {@link JSplitButton} popup menu.
*
* @param splitItem The {@link SplitItem} instance wrapping a {@link JMenuItem} as well as some associated {@link Action}s.
* @param <I> The {@link JMenuItem} type which should be added to this {@link JSplitButton} builder.
* @return This very instance, which enables builder-style method chaining.
*/
public <I extends JMenuItem> UIForSplitButton<B> add( SplitItem<I> splitItem ) {
NullUtil.nullArgCheck(splitItem, "buttonItem", SplitItem.class);
return _with( thisComponent -> {
_addSplitItem(splitItem, thisComponent);
})
._this();
}
private <I extends JMenuItem> void _addSplitItem( SplitItem<I> splitItem, B thisComponent ) {
I item = splitItem.getItem();
splitItem.getIsEnabled().ifPresent( isEnabled -> {
_onShow( isEnabled, thisComponent, (c,v) -> item.setEnabled(v) );
});
ExtraState state = ExtraState.of(thisComponent);
if ( item.isSelected() )
state.lastSelected[0] = item;
state.popupMenu.add(item);
state.options.put(item, ( (SplitItem<JMenuItem>) splitItem).getOnClick());
item.addActionListener(
e -> _runInApp(()->{
state.lastSelected[0] = item;
item.setSelected(true);
SplitItemDelegate<I> delegate =
new SplitItemDelegate<>(
e,
thisComponent,
() -> state.options.keySet().stream().map(o -> (I) o ).collect(Collectors.toList()),
item
);
state.onSelections.forEach(action -> {
try {
action.accept(new SplitButtonDelegate<>( thisComponent,(SplitItemDelegate<JMenuItem>) delegate ));
} catch (Exception exception) {
log.error("Error while executing selection action listener.", exception);
}
});
splitItem.getOnSelected().accept(delegate);
})
);
}
private static class ExtraState
{
static ExtraState of( JSplitButton pane ) {
return of(pane, state->{});
}
static ExtraState of( JSplitButton pane, Consumer<ExtraState> ini ) {
return ComponentExtension.from(pane)
.getOrSet(ExtraState.class, ()->{
ExtraState s = new ExtraState();
ini.accept(s);
return s;
});
}
final JPopupMenu popupMenu = new JPopupMenu();
final Map<JMenuItem, Action<SplitItemDelegate<JMenuItem>>> options = new LinkedHashMap<>(16);
final JMenuItem[] lastSelected = {null};
final List<Action<SplitButtonDelegate<JMenuItem>>> onSelections = new ArrayList<>();
}
}