package org.leumasjaffe.recipe.view; import java.awt.Component; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import javax.swing.JPanel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.jdesktop.swingx.VerticalLayout; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.experimental.FieldDefaults; import lombok.experimental.NonFinal; @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true) public class BetterAutoGrowPanel extends JPanel { private static interface SetGap { void setGap(int gap); } public static interface ChildComponent { void addGrowShrinkListener(DocumentListener dl); void removeGrowShrinkListener(DocumentListener dl); default void setListPosition(int zeroIndex) {} } @RequiredArgsConstructor @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true) private class GrowOnData implements DocumentListener { Supplier makeEmptyModel; Function makeComponent; @NonFinal T model = null; @Override public void changedUpdate(DocumentEvent e) {} @Override public void removeUpdate(DocumentEvent e) {} @Override public void insertUpdate(DocumentEvent e) { if (model != null) { models.add(model); last().removeGrowShrinkListener(this); last().addGrowShrinkListener(new ShrinkOnEmpty(last())); } model = makeEmptyModel.get(); final C comp = makeComponent.apply(model); comp.addGrowShrinkListener(this); members.add(comp); add(comp); callback.accept(true); } } @AllArgsConstructor private class ShrinkOnEmpty implements DocumentListener { ChildComponent component; @Override public void insertUpdate(DocumentEvent e) {} @Override public void changedUpdate(DocumentEvent e) {} @Override public void removeUpdate(DocumentEvent e) { if (e.getDocument().getLength() > 0) { return; } component.removeGrowShrinkListener(this); remove(members.indexOf(component)); callback.accept(false); } } @Delegate(types={SetGap.class}) VerticalLayout layout = new VerticalLayout(); List members = new ArrayList<>(); GrowOnData grow; @NonFinal List models = null; @NonFinal Consumer callback = (b) -> {}; /** * * @param makeEmptyModel A function to produce a blank model object for display. * If the model is updated in such a way as to be non-empty, it will be inserted * into the list of models. * @param makeComponent A function to generate a UI object given a model. The * object must meet the ChildComponent interface to install the AutoGrowPanel's * growing/shrinking listener objects. */ public BetterAutoGrowPanel(final @NonNull Supplier makeEmptyModel, final @NonNull Function makeComponent) { setLayout(layout); this.grow = new GrowOnData(makeEmptyModel, makeComponent); this.grow.insertUpdate(null); } /** * Activate this component against the target list of children * @param models A mutable list object containing the "child models" to be * rendered in this component */ public void setModel(final @NonNull List models) { setModel(models, (b) -> {}); } /** * * @param models A mutable list object containing the "child models" to be * rendered in this component * @param callback A callback that will be invoked each time a child is * added or removed. This allows us to provide some custom interactions * with the parent's context in case other actions need to occur. */ public void setModel(final @NonNull List models, final Consumer callback) { this.models = models; this.callback = callback; this.members.subList(0, lastIndex()).clear(); models.forEach(model -> { final C comp = this.grow.makeComponent.apply(model); comp.addGrowShrinkListener(new ShrinkOnEmpty(comp)); add(comp, lastIndex()); this.members.add(lastIndex(), comp); }); } @Override public void remove(final int index) { super.remove(index); members.remove(index); for (int size = lastIndex(); size >= index; --size) { members.get(index).setListPosition(index); } models.remove(index); } private int lastIndex() { return members.size() - 1; } private ChildComponent last() { return members.get(lastIndex()); } }