AutoGrowPanel.java 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package org.leumasjaffe.recipe.view;
  2. import java.awt.Component;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.function.Consumer;
  6. import java.util.function.Function;
  7. import java.util.function.Supplier;
  8. import javax.swing.JPanel;
  9. import javax.swing.event.DocumentEvent;
  10. import javax.swing.event.DocumentListener;
  11. import org.jdesktop.swingx.VerticalLayout;
  12. import lombok.AccessLevel;
  13. import lombok.AllArgsConstructor;
  14. import lombok.NonNull;
  15. import lombok.RequiredArgsConstructor;
  16. import lombok.experimental.Delegate;
  17. import lombok.experimental.FieldDefaults;
  18. import lombok.experimental.NonFinal;
  19. @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
  20. public class AutoGrowPanel<C extends Component & AutoGrowPanel.ChildComponent, T> extends JPanel {
  21. /**
  22. *
  23. */
  24. private static final long serialVersionUID = 3815045801030954255L;
  25. private static interface SetGap { void setGap(int gap); }
  26. public static interface ChildComponent {
  27. void addGrowShrinkListener(DocumentListener dl);
  28. void removeGrowShrinkListener(DocumentListener dl);
  29. default void setListPosition(int zeroIndex) {}
  30. }
  31. @RequiredArgsConstructor
  32. @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
  33. private class GrowOnData implements DocumentListener {
  34. Supplier<T> makeEmptyModel;
  35. Function<T, C> makeComponent;
  36. @NonFinal T model = null;
  37. @Override public void changedUpdate(DocumentEvent e) {}
  38. @Override public void removeUpdate(DocumentEvent e) {}
  39. @Override
  40. public void insertUpdate(DocumentEvent e) {
  41. if (model != null) {
  42. models.add(model);
  43. last().removeGrowShrinkListener(this);
  44. last().addGrowShrinkListener(new ShrinkOnEmpty(last()));
  45. }
  46. model = makeEmptyModel.get();
  47. final C comp = makeComponent.apply(model);
  48. members.add(comp);
  49. add(comp);
  50. comp.addGrowShrinkListener(this);
  51. comp.setListPosition(lastIndex());
  52. callback.accept(true);
  53. }
  54. }
  55. @AllArgsConstructor
  56. private class ShrinkOnEmpty implements DocumentListener {
  57. ChildComponent component;
  58. @Override public void insertUpdate(DocumentEvent e) {}
  59. @Override public void changedUpdate(DocumentEvent e) {}
  60. @Override
  61. public void removeUpdate(DocumentEvent e) {
  62. if (e.getDocument().getLength() > 0) {
  63. return;
  64. }
  65. component.removeGrowShrinkListener(this);
  66. remove(members.indexOf(component));
  67. callback.accept(false);
  68. }
  69. }
  70. @Delegate(types={SetGap.class})
  71. VerticalLayout layout = new VerticalLayout();
  72. List<ChildComponent> members = new ArrayList<>();
  73. GrowOnData grow;
  74. @NonFinal List<T> models = null;
  75. @NonFinal Consumer<Boolean> callback = (b) -> {};
  76. /**
  77. *
  78. * @param makeEmptyModel A function to produce a blank model object for display.
  79. * If the model is updated in such a way as to be non-empty, it will be inserted
  80. * into the list of models.
  81. * @param makeComponent A function to generate a UI object given a model. The
  82. * object must meet the ChildComponent interface to install the AutoGrowPanel's
  83. * growing/shrinking listener objects.
  84. */
  85. public AutoGrowPanel(final @NonNull Supplier<T> makeEmptyModel,
  86. final @NonNull Function<T, C> makeComponent) {
  87. setLayout(layout);
  88. this.grow = new GrowOnData(makeEmptyModel, makeComponent);
  89. this.grow.insertUpdate(null);
  90. }
  91. /**
  92. * Activate this component against the target list of children
  93. * @param models A mutable list object containing the "child models" to be
  94. * rendered in this component
  95. */
  96. public void setModel(final @NonNull List<T> models) {
  97. setModel(models, (b) -> {});
  98. }
  99. /**
  100. *
  101. * @param models A mutable list object containing the "child models" to be
  102. * rendered in this component
  103. * @param callback A callback that will be invoked each time a child is
  104. * added or removed. This allows us to provide some custom interactions
  105. * with the parent's context in case other actions need to occur.
  106. */
  107. public void setModel(final @NonNull List<T> models,
  108. final Consumer<Boolean> callback) {
  109. this.models = models;
  110. this.callback = callback;
  111. this.members.subList(0, lastIndex()).clear();
  112. models.forEach(model -> {
  113. final C comp = this.grow.makeComponent.apply(model);
  114. comp.addGrowShrinkListener(new ShrinkOnEmpty(comp));
  115. add(comp, lastIndex());
  116. comp.setListPosition(lastIndex());
  117. this.members.add(lastIndex(), comp);
  118. });
  119. last().setListPosition(lastIndex());
  120. }
  121. @Override
  122. public void remove(final int index) {
  123. super.remove(index);
  124. members.remove(index);
  125. for (int size = lastIndex(); size >= index; --size) {
  126. members.get(index).setListPosition(index);
  127. }
  128. models.remove(index);
  129. }
  130. private int lastIndex() { return members.size() - 1; }
  131. private ChildComponent last() {
  132. return members.get(lastIndex());
  133. }
  134. }