|
|
@@ -5,8 +5,6 @@ import java.util.ArrayList;
|
|
|
import java.util.List;
|
|
|
import java.util.function.Consumer;
|
|
|
import java.util.function.Function;
|
|
|
-import java.util.function.IntConsumer;
|
|
|
-import java.util.function.IntFunction;
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
import javax.swing.JPanel;
|
|
|
@@ -14,105 +12,145 @@ import javax.swing.event.DocumentEvent;
|
|
|
import javax.swing.event.DocumentListener;
|
|
|
|
|
|
import org.jdesktop.swingx.VerticalLayout;
|
|
|
-import org.leumasjaffe.event.AnyActionDocumentListener;
|
|
|
|
|
|
+import lombok.AccessLevel;
|
|
|
import lombok.AllArgsConstructor;
|
|
|
+import lombok.NonNull;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.experimental.Delegate;
|
|
|
+import lombok.experimental.FieldDefaults;
|
|
|
+import lombok.experimental.NonFinal;
|
|
|
|
|
|
-@SuppressWarnings("serial")
|
|
|
-public class AutoGrowPanel extends JPanel {
|
|
|
- public static interface DocumentListenable {
|
|
|
- void addDocumentListener(DocumentListener dl);
|
|
|
- void removeDocumentListener(DocumentListener dl);
|
|
|
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
|
|
|
+public class AutoGrowPanel<C extends Component & AutoGrowPanel.ChildComponent, T> extends JPanel {
|
|
|
+ /**
|
|
|
+ *
|
|
|
+ */
|
|
|
+ private static final long serialVersionUID = 3815045801030954255L;
|
|
|
+
|
|
|
+ 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) {}
|
|
|
}
|
|
|
|
|
|
- @AllArgsConstructor
|
|
|
- private class DeleteOnEmpty implements AnyActionDocumentListener {
|
|
|
- DocumentListenable content;
|
|
|
- @Override public void update(DocumentEvent e) {
|
|
|
- if (e.getDocument().getLength() == 0) {
|
|
|
- content.removeDocumentListener(this);
|
|
|
- int index = members.indexOf(content);
|
|
|
- members.remove(index);
|
|
|
- onDelete.accept(index);
|
|
|
- for (final int size = members.size(); index < size; ++index) {
|
|
|
- members.get(index).setListPosition(index);
|
|
|
- }
|
|
|
- remove((Component) content);
|
|
|
- validateNthParent(4);
|
|
|
+ @RequiredArgsConstructor
|
|
|
+ @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
|
|
|
+ private class GrowOnData implements DocumentListener {
|
|
|
+ Supplier<T> makeEmptyModel;
|
|
|
+ Function<T, C> 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 ExtendAction<T, C extends Component & DocumentListenable> implements AnyActionDocumentListener {
|
|
|
- final Function<T, C> factory;
|
|
|
- final Consumer<? super T> previous;
|
|
|
- final Supplier<? extends T> next;
|
|
|
- T current = null;
|
|
|
+ private class ShrinkOnEmpty implements DocumentListener {
|
|
|
+ ChildComponent component;
|
|
|
+
|
|
|
+ @Override public void insertUpdate(DocumentEvent e) {}
|
|
|
+ @Override public void changedUpdate(DocumentEvent e) {}
|
|
|
|
|
|
@Override
|
|
|
- public void update(DocumentEvent e) {
|
|
|
- previous.accept(current);
|
|
|
-
|
|
|
- final C object = factory.apply(current = next.get());
|
|
|
- final DocumentListenable back = getBack();
|
|
|
-
|
|
|
- back.removeDocumentListener(this);
|
|
|
- back.addDocumentListener(new DeleteOnEmpty(back));
|
|
|
- object.addDocumentListener(this);
|
|
|
+ public void removeUpdate(DocumentEvent e) {
|
|
|
+ if (e.getDocument().getLength() > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- members.add(object);
|
|
|
- add(object);
|
|
|
-
|
|
|
- validateNthParent(4);
|
|
|
+ component.removeGrowShrinkListener(this);
|
|
|
+ remove(members.indexOf(component));
|
|
|
+ callback.accept(false);
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
- IntFunction<DocumentListenable> prod;
|
|
|
- AnyActionDocumentListener grow;
|
|
|
- IntConsumer onDelete;
|
|
|
- List<DocumentListenable> members = new ArrayList<>();
|
|
|
+ @Delegate(types={SetGap.class})
|
|
|
+ VerticalLayout layout = new VerticalLayout();
|
|
|
+ List<ChildComponent> members = new ArrayList<>();
|
|
|
+ GrowOnData grow;
|
|
|
|
|
|
- @SafeVarargs
|
|
|
- public <T, C extends Component & DocumentListenable> AutoGrowPanel(Function<T, C> function,
|
|
|
- Supplier<T> newItem, Consumer<? super T> onData, IntConsumer onDelete, int gap, T...ts) {
|
|
|
- setLayout(new VerticalLayout(gap));
|
|
|
-
|
|
|
- T next = newItem.get();
|
|
|
- this.onDelete = onDelete;
|
|
|
- this.grow = new ExtendAction<T, C>(function, onData, newItem, next);
|
|
|
-
|
|
|
- for (T value : ts) {
|
|
|
- C listen = function.apply(value);
|
|
|
- members.add(listen);
|
|
|
- add(listen);
|
|
|
- listen.addDocumentListener(new DeleteOnEmpty(listen));
|
|
|
- }
|
|
|
-
|
|
|
- C empty = function.apply(next);
|
|
|
- members.add(empty);
|
|
|
- add(empty);
|
|
|
- empty.addDocumentListener(this.grow);
|
|
|
-
|
|
|
- for (int i = 0; i < members.size(); ++i) {
|
|
|
- members.get(i).setListPosition(i);
|
|
|
- }
|
|
|
+ @NonFinal List<T> models = null;
|
|
|
+ @NonFinal Consumer<Boolean> 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 AutoGrowPanel(final @NonNull Supplier<T> makeEmptyModel,
|
|
|
+ final @NonNull Function<T, C> makeComponent) {
|
|
|
+ setLayout(layout);
|
|
|
+ this.grow = new GrowOnData(makeEmptyModel, makeComponent);
|
|
|
+ this.grow.insertUpdate(null);
|
|
|
}
|
|
|
-
|
|
|
- private DocumentListenable getBack() {
|
|
|
- return members.get(members.size() - 1);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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<T> models) {
|
|
|
+ setModel(models, (b) -> {});
|
|
|
}
|
|
|
|
|
|
- private void validateNthParent(int i) {
|
|
|
- Component current = getParent();
|
|
|
- Component next = current;
|
|
|
- while (i --> 0 && next != null) {
|
|
|
- current = next;
|
|
|
- next = current.getParent();
|
|
|
+ /**
|
|
|
+ *
|
|
|
+ * @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<T> models,
|
|
|
+ final Consumer<Boolean> 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);
|
|
|
}
|
|
|
- current.validate();
|
|
|
+ models.remove(index);
|
|
|
}
|
|
|
|
|
|
+ private int lastIndex() { return members.size() - 1; }
|
|
|
+
|
|
|
+ private ChildComponent last() {
|
|
|
+ return members.get(lastIndex());
|
|
|
+ }
|
|
|
}
|