UIForScrollPanels.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.*;
import sprouts.impl.SequenceDiff;
import sprouts.impl.SequenceDiffOwner;
import swingtree.api.mvvm.EntryViewModel;
import swingtree.api.mvvm.ViewSupplier;
import swingtree.components.JScrollPanels;
import swingtree.layout.AddConstraint;
import javax.swing.*;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* A builder node for {@link JScrollPanels}, a custom SwingTree component,
* which is similar to a {@link JList} but with the ability to interact with
* the individual components in the list.
*
* @param <P> The type of the component which this builder node wraps.
* @author Daniel Nepp
*/
public class UIForScrollPanels<P extends JScrollPanels> extends UIForAnyScrollPane<UIForScrollPanels<P>, P>
{
private static final Logger log = LoggerFactory.getLogger(UIForScrollPanels.class);
private final BuilderState<P> _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 build.
*/
protected UIForScrollPanels( BuilderState<P> state ) {
Objects.requireNonNull(state);
_state = state;
}
@Override
protected BuilderState<P> _state() {
return _state;
}
@Override
protected UIForScrollPanels<P> _newBuilderWithState( BuilderState<P> newState ) {
return new UIForScrollPanels<>(newState);
}
@Override
protected void _addComponentTo(P thisComponent, JComponent addedComponent, @Nullable AddConstraint constraints) {
Objects.requireNonNull(addedComponent);
EntryViewModel entry = _entryModel();
if ( constraints == null )
thisComponent.addEntry( entry, m -> UI.of(addedComponent) );
else
thisComponent.addEntry( constraints, entry, m -> UI.of(addedComponent) );
}
private static EntryViewModel _entryModel() {
Var<Boolean> selected = Var.of(false);
Var<Integer> position = Var.of(0);
return new EntryViewModel() {
@Override public Var<Boolean> isSelected() { return selected; }
@Override public Var<Integer> position() { return position; }
};
}
private static <M> M _modelFetcher(int i, Vals<M> vals) {
M v = vals.at(i).get();
if ( v instanceof EntryViewModel ) ((EntryViewModel) v).position().set(i);
return v;
}
private static <M> M _entryFetcher(int i, Vals<M> vals) {
M v = _modelFetcher(i, vals);
return ( v != null ? (M) v : (M)_entryModel() );
}
@Override
protected <M> void _addViewableProps(
Vals<M> models, @Nullable AddConstraint attr, ModelToViewConverter<M> viewSupplier, P thisComponent
) {
BiConsumer<Integer, Vals<M>> addAllAt = (index, vals) -> {
boolean allAreEntries = vals.stream().allMatch( v -> v instanceof EntryViewModel );
if ( allAreEntries ) {
List<EntryViewModel> entries = (List) vals.toList();
thisComponent.addAllEntriesAt(index, attr, entries, (ViewSupplier<EntryViewModel>) viewSupplier);
}
else
for ( int i = 0; i< vals.size(); i++ ) {
int finalI = i;
thisComponent.addEntryAt(
finalI + index, attr,
_entryModel(),
m -> viewSupplier.createViewFor(_entryFetcher(finalI,vals))
);
}
};
_onShow( models, thisComponent, (c, delegate) -> {
viewSupplier.rememberCurrentViewsForReuse();
Tuple<M> tupleOfModels = Tuple.of(delegate.currentValues().type(), delegate.currentValues());
int delegateIndex = delegate.index().orElse(-1);
SequenceChange changeType = delegate.change();
int removeCount = delegate.oldValues().size();
int addCount = delegate.newValues().size();
int maxChange = Math.max(removeCount, addCount);
_update(c, attr, changeType, delegateIndex, maxChange, tupleOfModels, viewSupplier);
viewSupplier.clearCurrentViews();
});
addAllAt.accept(0,models);
}
private static <M> M _modelFetcher(int i, Tuple<M> tuple) {
M v = tuple.get(i);
if ( v instanceof EntryViewModel ) ((EntryViewModel) v).position().set(i);
return v;
}
private static <M> M _entryFetcher(int i, Tuple<M> tuple) {
M v = _modelFetcher(i, tuple);
return ( v != null ? (M) v : (M)_entryModel() );
}
private <M> void _addAllEntriesAt(
@Nullable AddConstraint attr,
JScrollPanels thisComponent,
int index,
Iterable<M> iterable,
ViewSupplier<M> viewSupplier
) {
boolean allAreEntries = StreamSupport.stream(iterable.spliterator(), false).allMatch( v -> v instanceof EntryViewModel );
if ( allAreEntries ) {
List<EntryViewModel> entries = StreamSupport.stream(iterable.spliterator(), false).map(v -> (EntryViewModel)v).collect(Collectors.toList());
thisComponent.addAllEntriesAt(index, attr, entries, (ViewSupplier<EntryViewModel>) viewSupplier);
}
else {
Tuple<M> tuple = (iterable instanceof Tuple) ? (Tuple<M>) iterable : (Tuple<M>) Tuple.of(Object.class, (Iterable<Object>) iterable);
for ( int i = 0; i< tuple.size(); i++ ) {
int finalI = i;
thisComponent.addEntryAt(
i + index, attr,
_entryModel(),
m -> viewSupplier.createViewFor(_entryFetcher(finalI,tuple))
);
}
}
}
private <M> void _setAllEntriesAt(
@Nullable AddConstraint attr,
JScrollPanels thisComponent,
int index,
Iterable<M> iterable,
ViewSupplier<M> viewSupplier
) {
boolean allAreEntries = StreamSupport.stream(iterable.spliterator(), false).allMatch( v -> v instanceof EntryViewModel );
if ( allAreEntries ) {
List<EntryViewModel> entries = StreamSupport.stream(iterable.spliterator(), false).map(v -> (EntryViewModel)v).collect(Collectors.toList());
thisComponent.setAllEntriesAt(index, attr, entries, (ViewSupplier<EntryViewModel>) viewSupplier);
}
else {
Tuple<M> tuple = (iterable instanceof Tuple) ? (Tuple<M>) iterable : (Tuple<M>) Tuple.of(Object.class, (Iterable<Object>) iterable);
for (int i = 0; i < tuple.size(); i++) {
int finalI = i;
thisComponent.setEntryAt(
i + index, attr,
_entryModel(),
m -> viewSupplier.createViewFor(_entryFetcher(finalI, tuple))
);
}
}
}
@Override
protected <M> void _addViewableProps(
Val<Tuple<M>> models,
@Nullable AddConstraint attr,
ModelToViewConverter<M> viewSupplier,
P thisComponent
) {
AtomicReference<@Nullable SequenceDiff> lastDiffRef = new AtomicReference<>(null);
if (models.get() instanceof SequenceDiffOwner)
lastDiffRef.set(((SequenceDiffOwner)models.get()).differenceFromPrevious().orElse(null));
_onShow( models, thisComponent, (c, tupleOfModels) -> {
viewSupplier.rememberCurrentViewsForReuse();
SequenceDiff diff = null;
SequenceDiff lastDiff = lastDiffRef.get();
if (tupleOfModels instanceof SequenceDiffOwner)
diff = ((SequenceDiffOwner)tupleOfModels).differenceFromPrevious().orElse(null);
lastDiffRef.set(diff);
if ( diff == null || ( lastDiff == null || !diff.isDirectSuccessorOf(lastDiff) ) ) {
c.removeAllEntries();
_addAllEntriesAt(attr, c, 0, tupleOfModels, viewSupplier);
} else {
int index = diff.index().orElse(-1);
int count = diff.size();
_update(c, attr, diff.change(), index, count, tupleOfModels, viewSupplier);
}
viewSupplier.clearCurrentViews();
});
models.ifPresent( (tupleOfModels) -> {
thisComponent.removeAllEntries();
_addAllEntriesAt(attr, thisComponent, 0, tupleOfModels, viewSupplier);
});
}
private <M> void _update(
P c,
@Nullable AddConstraint attr,
SequenceChange change,
int index,
int count,
Tuple<M> tupleOfModels,
ModelToViewConverter<M> viewSupplier
) {
if ( index < 0 ) {
// We do a simple re-build
c.removeAllEntries();
_addAllEntriesAt(attr, c, 0, tupleOfModels, viewSupplier);
} else {
switch (change) {
case SET:
Tuple<M> slice = tupleOfModels.slice(index, index+count);
_setAllEntriesAt(attr, c, index, slice, viewSupplier);
break;
case ADD:
_addAllEntriesAt(attr, c, index, tupleOfModels.slice(index, index+count), viewSupplier);
break;
case REMOVE:
c.removeEntriesAt(index, count);
break;
case RETAIN: // Only keep the elements in the range.
// Remove trailing components:
c.removeEntriesAt(index + count, c.getNumberOfEntries() - (index + count));
// Remove leading components:
c.removeEntriesAt(0, index);
break;
case CLEAR:
c.removeAllEntries();
break;
case NONE:
break;
default:
log.error("Unknown change type: {}", change, new Throwable());
// We do a simple rebuild:
c.removeAllEntries();
_addAllEntriesAt(attr, c, 0, tupleOfModels, viewSupplier);
}
}
}
}