
swingtree.UIForCombo Maven / Gradle / Ivy
package swingtree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Action;
import sprouts.*;
import swingtree.api.Configurator;
import javax.swing.*;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A SwingTree builder node designed for configuring {@link JComboBox} instances.
*
* Please take a look at the living swing-tree documentation
* where you can browse a large collection of examples demonstrating how to use the API of this class.
*/
public final class UIForCombo> extends UIForAnySwing, JComboBox>
{
private static final Logger log = LoggerFactory.getLogger(UIForCombo.class);
private final BuilderState> _state;
UIForCombo( BuilderState> state ) {
Objects.requireNonNull(state);
_state = state;
}
/**
* Builds and returns the configured {@link JComboBox} instance.
*
* @return The configured {@link JComboBox} instance.
*/
public JComboBox getComboBox() {
return this.get(getType());
}
@Override
protected BuilderState> _state() {
return _state;
}
@Override
protected UIForCombo _newBuilderWithState(BuilderState> newState ) {
return new UIForCombo<>(newState);
}
private void _bindComboModelToEditor( JComboBox thisComponent, AbstractComboModel model ) {
Component editor = thisComponent.getEditor().getEditorComponent();
if ( editor instanceof JTextField ) {
JTextField field = (JTextField) editor;
boolean[] comboIsOpen = {false};
WeakReference> weakCombo = new WeakReference<>(thisComponent);
UI.of(field).onTextChange( it -> {
JComboBox strongCombo = weakCombo.get();
if ( !comboIsOpen[0] && strongCombo != null && strongCombo.isEditable() )
model.setFromEditor(field.getText());
});
_onShow( model._getSelectedItemVar(), thisComponent, (c, v) ->
model.doQuietly(()->{
c.getEditor().setItem(v);
model.fireListeners();
})
);
// Adds a PopupMenu listener which will listen to notification
// messages from the popup portion of the combo box.
thisComponent.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
// This method is called before the popup menu becomes visible.
comboIsOpen[0] = true;
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// This method is called before the popup menu becomes invisible
comboIsOpen[0] = false;
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
// This method is called when the popup menu is canceled
comboIsOpen[0] = false;
}
});
}
}
/**
* Registers a listener to be notified when the combo box is opened,
* meaning its popup menu is shown after the user clicks on the combo box.
*
* @param action the action to be executed when the combo box is opened.
* @return this
*/
public UIForCombo onOpen( Action> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupOpen(thisComponent, e -> _runInApp(()->action.accept(new ComponentDelegate<>( (C) thisComponent, e )) ) );
})
._this();
}
private void _onPopupOpen( JComboBox thisComponent, Consumer consumer ) {
thisComponent.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 combo box is closed,
* meaning its popup menu is hidden after the user clicks on the combo box.
*
* @param action the action to be executed when the combo box is closed.
* @return this
*/
public UIForCombo onClose( Action> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupClose(thisComponent,
e -> _runInApp(()->action.accept(new ComponentDelegate<>( (C) thisComponent, e )) )
);
})
._this();
}
private void _onPopupClose( JComboBox thisComponent, Consumer consumer ) {
thisComponent.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {/* Not relevant here */}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
consumer.accept(e); // This method is called before the popup menu becomes invisible
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {/* Not relevant here */}
});
}
/**
* Registers a listener to be notified when the combo box is canceled,
* meaning its popup menu is hidden which
* typically happens when the user clicks outside the combo box.
*
* @param action the action to be executed when the combo box is canceled.
* @return this
*/
public UIForCombo onCancel( Action> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onPopupCancel(thisComponent,
e -> _runInApp(()->action.accept(new ComponentDelegate<>( (C) thisComponent, e )) )
);
})
._this();
}
private void _onPopupCancel( JComboBox thisComponent, Consumer consumer ) {
thisComponent.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) {
consumer.accept(e); // This method is called when the popup menu is canceled
}
});
}
/**
* Adds an {@link Action} to the underlying {@link JComboBox}
* through an {@link java.awt.event.ActionListener},
* which will be called when a selection has been made. If the combo box is editable, then
* an {@link ActionEvent} will be fired when editing has stopped.
* For more information see {@link JComboBox#addActionListener(ActionListener)}.
*
* @param action The {@link Action} that will be notified.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code action} is {@code null}.
*/
public UIForCombo onSelection( Action, ActionEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onSelection(thisComponent,
e -> _runInApp(()->action.accept(new ComponentDelegate<>( thisComponent, e )))
);
})
._this();
}
private void _onSelection( JComboBox thisComponent, Consumer consumer ) {
/*
When an action event is fired, Swing will go through all the listeners
from the most recently added to the first added. This means that if we simply add
a listener through the "addActionListener" method, we will be the last to be notified.
This is problematic because it is built on the assumption that the last listener
added is more interested in the event than the first listener added.
This however is an unintuitive assumption, meaning a user would expect
the first listener added to be the most interested in the event
simply because it was added first.
This is especially true in the context of declarative UI design.
*/
ActionListener[] listeners = thisComponent.getActionListeners();
for (ActionListener listener : listeners)
thisComponent.removeActionListener(listener);
thisComponent.addActionListener(e -> {
/*
Unfortunately, this event is fired in all kinds of
annoying situations, so we need to filter things that
are only relevant to us...
We know that when the action command is "comboBoxEdited", then
the user wrote into the text field.
This is also fired when the user presses enter!
So we filter the event:
*/
if ( "comboBoxEdited".equals(e.getActionCommand()) )
return;
/*
Now another big problem is that when a user types
something, the editor will inform our combo box model
of the change and then the model will trigger item change listeners.
Unfortunately, this will then cause a domino effect, because
the combo box will consequently trigger an
action event with the action command "comboBoxChanged".
We can filter the event by checking if it comes from
our model:
*/
if ( e.getSource() instanceof JComboBox ) {
ComboBoxModel> model = ((JComboBox>) e.getSource()).getModel();
if ( model instanceof AbstractComboModel ) {
AbstractComboModel> swingTreeModel = (AbstractComboModel>) model;
if ( !swingTreeModel.acceptsEditorChanges() )
return;
}
}
consumer.accept(e);
});
for ( int i = listeners.length - 1; i >= 0; i-- ) // reverse order because swing does not give us the listeners in the order they were added!
thisComponent.addActionListener(listeners[i]);
}
/**
* Adds an {@link ActionListener} to the editor component of the underlying {@link JComboBox}
* which will be called when a selection has been made. If the combo box is editable, then
* an {@link ActionEvent} will be fired when editing has stopped.
* For more information see {@link JComboBox#addActionListener(ActionListener)}.
*
* @param action The {@link Action} that will be notified.
* @return This very builder instance, which allows for method chaining.
**/
public UIForCombo onEnter( Action> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
_onEnter(thisComponent, e -> _runInApp(()->action.accept(new ComponentDelegate<>( (C) thisComponent, e ))) );
})
._this();
}
private void _onEnter( JComboBox thisComponent, Consumer consumer ) {
Component editor = thisComponent.getEditor().getEditorComponent();
if ( editor instanceof JTextField ) {
JTextField field = (JTextField) editor;
UI.of(field).onEnter( it -> consumer.accept(it.getEvent()) );
}
}
/**
* Use this to enable or disable editing for the wrapped UI component.
*
* @param isEditable The truth value determining if the UI component should be editable or not.
* @return This very instance, which enables builder-style method chaining.
*/
public UIForCombo isEditableIf( boolean isEditable ) {
return _with( thisComponent -> {
thisComponent.setEditable(isEditable);
})
._this();
}
/**
* Use this to enable or disable editing of the wrapped UI component
* through property binding dynamically.
*
* @param isEditable The boolean property determining if the UI component should be editable or not.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code isEditable} is {@code null}.
*/
public UIForCombo isEditableIf( Var isEditable ) {
NullUtil.nullPropertyCheck(isEditable, "isEditable", "Null is not a valid state for modelling 'isEditable''.");
return _withOnShow( isEditable, (thisComponent, v) -> {
thisComponent.setEditable(v);
})
._with( thisComponent -> {
thisComponent.setEditable(isEditable.get());
})
._this();
}
public final UIForCombo _withRenderer( RenderBuilder renderBuilder ) {
NullUtil.nullArgCheck(renderBuilder, "renderBuilder", RenderBuilder.class);
return _with( thisComponent -> {
thisComponent.setRenderer((ListCellRenderer) renderBuilder.buildForCombo((C)thisComponent));
})
._this();
}
/**
* Use this to define a generic combo box renderer for various item types..
* You would typically want to use this method to render generic types where the only
* common type is {@link Object}, yet you still want to render the items
* in a specific way depending on their actual type.
* This is done like so:
* {@code
* UI.comboBox(new Object[]{":-)", 42L, '§'})
* .withRenderer( it -> it
* .when(String.class).asText( cell -> "String: "+cell.getValue() )
* .when(Character.class).asText( cell -> "Char: "+cell.getValue() )
* .when(Number.class).asText( cell -> "Number: "+cell.getValue() )
* );
* }
* Note that inside the lambda function, you can use the {@link RenderBuilder} to define
* for what type of item you want to render the item in a specific way and the {@link RenderAs}
* to define how the item should be rendered.
*
* You may want to know that a similar API is also available for the {@link javax.swing.JList}
* and {@link javax.swing.JTable} components, see {@link UIForList#withRenderer(Configurator)},
* {@link UIForTable#withRenderer(Configurator)} and {@link UI#table(Configurator)}
* for more information.
*
* @param renderBuilder A lambda function that configures the renderer for this combo box.
* @return This combo box instance for further configuration.
* @param The type of the value that is being rendered in this combo box.
*/
public final UIForCombo withRenderer(
Configurator> renderBuilder
) {
Class