UIForList.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Action;
import sprouts.*;
import swingtree.api.Configurator;
import swingtree.api.ListEntryDelegate;
import swingtree.api.ListEntryRenderer;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A SwingTree builder node designed for configuring {@link JList} instances.
* <p>
* <b>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 <L> The type of the {@link JList} instance which will be managed by this builder.
*/
public final class UIForList<E, L extends JList<E>> extends UIForAnySwing<UIForList<E, L>, L>
{
private static final Logger log = LoggerFactory.getLogger(UIForList.class);
private final BuilderState<L> _state;
/**
* Extensions of the {@link UIForAnySwing} always wrap
* a single component for which they are responsible.
*
* @param state The {@link BuilderState} modelling how the underlying component is built.
*/
UIForList( BuilderState<L> state ) {
Objects.requireNonNull(state);
_state = state;
}
@Override
protected BuilderState<L> _state() {
return _state;
}
@Override
protected UIForList<E, L> _newBuilderWithState(BuilderState<L> newState ) {
return new UIForList<>(newState);
}
/**
* Takes the provided list of entry objects and sets them as {@link JList} data.
*
* @param entries The list of entries to set as data.
* @return This instance of the builder node.
*/
public final UIForList<E, L> withEntries( List<E> entries ) {
return _with( thisComponent ->
thisComponent.setModel (
new AbstractListModel<E>() {
private List<E> _reference = new ArrayList<>(entries);
@Override
public int getSize() { _checkContentChange(); return entries.size(); }
@Override
public E getElementAt( int i ) { _checkContentChange(); return entries.get( i ); }
private void _checkContentChange() {
UI.runLater(()-> {
if ( _reference.size() != entries.size() ) {
fireContentsChanged( this, 0, entries.size() );
_reference = new ArrayList<>(entries);
}
else
for ( int i = 0; i < entries.size(); i++ )
if ( !_reference.get( i ).equals( entries.get( i ) ) ) {
fireContentsChanged( this, 0, entries.size() );
_reference = new ArrayList<>(entries);
break;
}
});
}
}
)
)
._this();
}
/**
* Takes the provided array of entry objects and sets them as {@link JList} data.
*
* @param entries The array of entries to set as data.
* @return This instance of the builder node.
*/
@SafeVarargs
public final UIForList<E, L> withEntries( E... entries ) {
return _with( thisComponent -> {
thisComponent.setListData( entries );
})
._this();
}
/**
* Takes the provided observable property list of entries in the form of a {@link Vals}
* object and uses them as a basis for modelling the {@link JList} data.
* If the {@link Vals} object changes, the {@link JList} data will be updated accordingly,
* and vice versa.
*
* @param entries The {@link Vals} of entries to set as data model.
* @return This instance of the builder node to allow for builder-style fluent method chaining.
*/
public final UIForList<E, L> withEntries( Vals<E> entries ) {
Objects.requireNonNull(entries, "entries");
ValsListModel<E> model = new ValsListModel<>(entries);
return _with( thisComponent -> {
thisComponent.setModel(model);
})
._withOnShow( entries, (thisComponent, v) -> {
model.fire(v);
})
._this();
}
/**
* Takes an observable property in the form of a {@link Var} object
* and uses it as a basis for modelling the {@link JList} selection.
* If the {@link Var} object changes, the {@link JList} selection will be updated accordingly,
* and vice versa.
* If you do not want this relationship to be bidirectional, use {@link #withSelection(Val)} instead.
*
* @param selection The {@link Var} of entries to set as selection model.
* @return This instance of the builder node to allow for fluent method chaining.
*/
public final UIForList<E, L> withSelection( Var<E> selection ) {
return _with( thisComponent -> {
thisComponent.addListSelectionListener( e -> {
if ( !e.getValueIsAdjusting() )
// Necessary because Java 8 does not check if index is out of bounds.
if (thisComponent.getMinSelectionIndex() >= thisComponent.getModel().getSize())
selection.set( From.VIEW, NullUtil.fakeNonNull(null) );
else
selection.set( From.VIEW, thisComponent.getSelectedValue() );
});
})
._withOnShow( selection, (thisComponent,v) -> {
if (v == null)
// Necessary because Java 8 does not handle null properly.
thisComponent.clearSelection();
else
thisComponent.setSelectedValue(v, true);
})
._with( thisComponent -> {
thisComponent.setSelectedValue( selection.orElseNull(), true );
})
._this();
}
/**
* Takes an observable read-only property in the form of a {@link Val} object
* and uses it as a basis for modelling the {@link JList} selection.
* If the {@link Val} object changes, the {@link JList} selection will be updated accordingly.
* However, if the {@link JList} selection changes due to user interaction,
* the {@link Val} object will not be updated.
*
* @param selection The {@link Val} of entries to set as selection model.
* @return This instance of the builder node to allow for fluent method chaining.
*/
public final UIForList<E, L> withSelection( Val<E> selection ) {
NullUtil.nullArgCheck(selection, "selection", Val.class);
return _withOnShow( selection, (thisComponent,v) -> {
thisComponent.setSelectedValue( v, true );
})
._with( thisComponent -> {
thisComponent.setSelectedValue( selection.orElseNull(), true );
})
._this();
}
/**
* The {@link ListEntryRenderer} passed to this method is a functional interface
* receiving a {@link ListEntryDelegate} instance and returns
* a {@link javax.swing.JComponent}, which is
* used to render each entry of the {@link JList} instance. <br>
* A typical usage of this method would look like this:
* <pre>{@code
* listOf(vm.colors())
* .withRenderComponent( it -> new Component() {
* {@literal @}Override
* public void paint(Graphics g) {
* g.setColor(it.entry().orElse(Color.PINK));
* g.fillRect(0,0,getWidth(),getHeight());
* }
* })
* }</pre>
* <p>
* In this example, a new {@link JList} is created for the observable property list
* of colors, which is provided by the <code>vm.colors()</code> method.
* The entries of said list are individually exposed specified renderer
* lambda expression, which return a {@link javax.swing.JComponent} instance
* that is used by the {@link JList} to render each entry.
* In this case a colored rectangle is rendered for each entry.
*
* @param renderer The {@link ListEntryRenderer} that will be used to supply {@link javax.swing.JComponent}s
* responsible for rendering each entry of the {@link JList} instance.
* @return This instance of the builder node to allow for fluent method chaining.
*/
public final UIForList<E, L> withRenderComponent( ListEntryRenderer<E, L> renderer ) {
return _with( thisComponent -> {
thisComponent.setCellRenderer((list, value, index, isSelected, cellHasFocus) -> renderer.render(new ListEntryDelegate<E, L>() {
@Override public L list() { return (L) list; }
@Override public Optional<E> entry() { return Optional.ofNullable(value); }
@Override public int index() { return index; }
@Override public boolean isSelected() { return isSelected; }
@Override public boolean hasFocus() { return cellHasFocus; }
}));
})
._this();
}
/**
* Adds an {@link Action} event handler to the underlying {@link JList}
* through a {@link javax.swing.event.ListSelectionListener},
* which will be called when a list selection has been made.
* {see JList#addListSelectionListener(ListSelectionListener)}.
*
* @param action The {@link Action} that will be notified.
* @return This very instance, which enables builder-style method chaining.
*/
public final UIForList<E, L> onSelection( Action<ComponentDelegate<JList<E>, ListSelectionEvent>> action ) {
NullUtil.nullArgCheck(action, "action", Action.class);
return _with( thisComponent -> {
thisComponent.addListSelectionListener(
e -> _runInApp(()->action.accept(new ComponentDelegate<>( thisComponent, e)))
);
})
._this();
}
private final <V extends E> UIForList<E, L> _withRenderer( RenderBuilder<L,V> renderBuilder ) {
NullUtil.nullArgCheck(renderBuilder, "renderBuilder", RenderBuilder.class);
return _with( thisComponent -> {
thisComponent.setCellRenderer((ListCellRenderer<E>) renderBuilder.buildForList(thisComponent));
})
._this();
}
/**
* Sets the {@link ListCellRenderer} for the {@link JList}, which renders the list items
* by supplying a custom component for each item through the
* {@link ListCellRenderer#getListCellRendererComponent(JList, Object, int, boolean, boolean)} method.
* <p>
* @param renderer The {@link ListCellRenderer} that will be used to paint each cell in the list.
* @return This very instance, which enables builder-style method chaining.
*/
public final UIForList<E, L> withCellRenderer( ListCellRenderer<E> renderer ) {
return _with( thisComponent -> {
thisComponent.setCellRenderer(renderer);
})
._this();
}
/**
* Use this to build a list cell renderer for various item types
* by defining a renderer for each type or using {@link Object} as a common type
* using the fluent builder API exposed to the {@link Configurator}
* lambda function passed to this method. <br>
* A typical usage may look something like this:
* <pre>{@code
* UI.list(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() )
* );
* }</pre>
* Note that a similar API is also available for the {@link javax.swing.JComboBox}
* and {@link javax.swing.JTable} components, see {@link UIForCombo#withRenderer(Configurator)},
* {@link UIForTable#withRenderer(Configurator)} and {@link UIForTable#withRendererForColumn(int, 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 <V> The type of the value that is being rendered in this combo box.
*/
public final <V extends E> UIForList<E, L> withRenderer(
Configurator<RenderBuilder<L,V>> renderBuilder
) {
Class<Object> commonType = Object.class;
Objects.requireNonNull(commonType);
RenderBuilder render = RenderBuilder.forList(commonType);
try {
render = renderBuilder.configure(render);
} catch (Exception e) {
log.error("Error while building renderer.", e);
return this;
}
Objects.requireNonNull(render);
return _withRenderer(render);
}
private static class ValsListModel<E> extends AbstractListModel<E>
{
private final Vals<E> _entries;
public ValsListModel( Vals<E> entries ) {
_entries = Objects.requireNonNull(entries, "entries");
}
@Override public int getSize() {
return _entries.size();
}
@Override public @Nullable E getElementAt(int i ) {
return _entries.at( i ).orElseNull();
}
public void fire( ValsDelegate<E> v ) {
int index = v.index();
if ( index < 0 ) {
fireContentsChanged( this, 0, _entries.size() );
return;
}
switch ( v.changeType() ) {
case ADD: fireIntervalAdded( this, index, index + v.newValues().size() ); break;
case REMOVE: fireIntervalRemoved( this, index, index + v.oldValues().size() ); break;
case SET: fireContentsChanged( this, index, index + v.newValues().size() ); break;
default:
fireContentsChanged( this, 0, _entries.size() );
}
}
}
}