Browse Source

Merge branch 'feat/view/observerBindings'

* feat/view/observerBindings:
  Make it so Products (and thus, Recipes) do not track objects by preparation when composing their ingredients list.
  Use ObservableController with an automagic value checker.
  Add separate versions of IngredientPanel for its three main use-cases: - Preparation - Cooking Steps - Summarizing Ingredients Needed
  Skip ingredients that have no prep in prep phase.
  Make PreparationPanel always match what is shown in the steps.
  Fix some bugs and update to observer:0.4.1
  Fix ProductSummaryPanel to be less bad.
  Update with new feature in observable:0.4.0
  Add harness to forward observation events up the stack.
  Add Observable project, mark Ingredients (IngredientPanel) and Products (ProductSummaryPanel).
Sam Jaffe 5 years ago
parent
commit
695aae6307

+ 5 - 0
pom.xml

@@ -55,6 +55,11 @@
     </plugins>
     </plugins>
   </build>
   </build>
   <dependencies>
   <dependencies>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>observer</artifactId>
+      <version>0.5.0</version>
+    </dependency>
     <dependency>
     <dependency>
       <groupId>org.leumasjaffe</groupId>
       <groupId>org.leumasjaffe</groupId>
       <artifactId>event</artifactId>
       <artifactId>event</artifactId>

+ 11 - 7
src/main/lombok/org/leumasjaffe/recipe/model/Card.java

@@ -5,10 +5,14 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 
 
+import org.leumasjaffe.observer.Observable;
+
 import lombok.Data;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
 
 
-@Data
-public class Card implements CompoundRecipeComponent {
+@Data @EqualsAndHashCode(callSuper=false)
+public class Card extends Observable.Instance implements CompoundRecipeComponent {	
 	int id = 0; // TODO Fix this
 	int id = 0; // TODO Fix this
 	int[] dependsOn = {}; // decltype(id)[]
 	int[] dependsOn = {}; // decltype(id)[]
 	String vessel = "";
 	String vessel = "";
@@ -21,10 +25,10 @@ public class Card implements CompoundRecipeComponent {
 	}
 	}
 	
 	
 	public Stream<? extends RecipeComponent> getComponents() {
 	public Stream<? extends RecipeComponent> getComponents() {
-		if (preparation.isPresent()) {
-			return Stream.of(preparation.get());
-		} else {
-			return cooking.stream();
-		}
+		return cooking.stream();
+	}
+	
+	public void setPreparation(final @NonNull Preparation p) {
+		preparation = Optional.of(new Preparation(p.duration, this::getIngredients));
 	}
 	}
 }
 }

+ 3 - 9
src/main/lombok/org/leumasjaffe/recipe/model/CompoundRecipeComponent.java

@@ -1,10 +1,10 @@
 package org.leumasjaffe.recipe.model;
 package org.leumasjaffe.recipe.model;
 
 
 import java.util.Collection;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 
 
+import org.leumasjaffe.recipe.util.Collator;
+
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 
 @JsonIgnoreProperties({"duration", "ingredients", "components", "ingredientsAsStream"})
 @JsonIgnoreProperties({"duration", "ingredients", "components", "ingredientsAsStream"})
@@ -20,12 +20,6 @@ interface CompoundRecipeComponent extends RecipeComponent {
 	
 	
 	@Override
 	@Override
 	default Collection<Ingredient> getIngredients() {
 	default Collection<Ingredient> getIngredients() {
-		final Map<String, Ingredient> map = new HashMap<>();
-        getIngredientsAsStream().forEach(i -> {
-        	final String key = i.key();
-            map.computeIfPresent(key, (k, v) -> v.plus(i));
-            map.computeIfAbsent(key, k -> i);
-        });
-        return map.values();
+		return Collator.collateBy(getIngredientsAsStream(), Ingredient::key, Ingredient::plus);
 	}
 	}
 }
 }

+ 5 - 2
src/main/lombok/org/leumasjaffe/recipe/model/Ingredient.java

@@ -1,11 +1,14 @@
 package org.leumasjaffe.recipe.model;
 package org.leumasjaffe.recipe.model;
 
 
+import org.leumasjaffe.observer.Observable;
+
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.NoArgsConstructor;
 
 
-@Data @AllArgsConstructor @NoArgsConstructor
-public class Ingredient {
+@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper=false)
+public class Ingredient extends Observable.Instance {
 	String name;
 	String name;
 	String preparation = "";
 	String preparation = "";
 	Amount amount;
 	Amount amount;

+ 16 - 3
src/main/lombok/org/leumasjaffe/recipe/model/Preparation.java

@@ -1,12 +1,25 @@
 package org.leumasjaffe.recipe.model;
 package org.leumasjaffe.recipe.model;
 
 
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 
-@Data
+@Data @AllArgsConstructor @NoArgsConstructor
 public class Preparation implements RecipeComponent {
 public class Preparation implements RecipeComponent {
-	List<Ingredient> ingredients = new ArrayList<>();
 	Duration duration;
 	Duration duration;
+	@JsonIgnore Supplier<Collection<Ingredient>> producer = Collections::emptyList;
+	
+	@JsonIgnore
+	public List<Ingredient> getIngredients() {
+		return producer.get().stream().filter(i -> !i.getPreparation().isEmpty())
+				.collect(Collectors.toList());
+	}
 }
 }

+ 12 - 3
src/main/lombok/org/leumasjaffe/recipe/model/Product.java

@@ -3,20 +3,29 @@ package org.leumasjaffe.recipe.model;
 import java.util.List;
 import java.util.List;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 
 
+import org.leumasjaffe.observer.Observable;
+import org.leumasjaffe.recipe.util.Collator;
+
 import lombok.Data;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 
-@Data
-public class Product implements CompoundRecipeComponent {
+@Data @EqualsAndHashCode(callSuper=false)
+public class Product extends Observable.Instance implements CompoundRecipeComponent {
 	String name;
 	String name;
 	List<Card> cards;
 	List<Card> cards;
 
 
 	@Override
 	@Override
 	public Stream<Ingredient> getIngredientsAsStream() {
 	public Stream<Ingredient> getIngredientsAsStream() {
-		return cards.stream().flatMap(Card::getIngredientsAsStream);
+		return Collator.collateBy(cards.stream().flatMap(Card::getIngredientsAsStream),
+				Product::key, Ingredient::plus).stream();
 	}
 	}
 
 
 	@Override
 	@Override
 	public Stream<? extends RecipeComponent> getComponents() {
 	public Stream<? extends RecipeComponent> getComponents() {
 		return cards.stream();
 		return cards.stream();
 	}
 	}
+	
+	private static String key(Ingredient ingredient) {
+		return ingredient.getName() + ingredient.getAmount().getUnit();
+	}
 }
 }

+ 5 - 2
src/main/lombok/org/leumasjaffe/recipe/model/Step.java

@@ -3,10 +3,13 @@ package org.leumasjaffe.recipe.model;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
+import org.leumasjaffe.observer.Observable;
+
 import lombok.Data;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 
-@Data
-public class Step implements RecipeComponent {
+@Data @EqualsAndHashCode(callSuper=false)
+public class Step extends Observable.Instance implements RecipeComponent {
 	List<Ingredient> ingredients = new ArrayList<>();
 	List<Ingredient> ingredients = new ArrayList<>();
 	Duration duration;
 	Duration duration;
 	String instruction;
 	String instruction;

+ 24 - 0
src/main/lombok/org/leumasjaffe/recipe/util/Collator.java

@@ -0,0 +1,24 @@
+package org.leumasjaffe.recipe.util;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class Collator {
+	public <T> Collection<T> collateBy(final Stream<T> stream, final Function<T, String> getKey,
+			final BiFunction<T, T, T> folder) {
+		final Map<String, T> map = new HashMap<>();
+        stream.forEach(value -> {
+        	final String key = getKey.apply(value);
+            map.computeIfPresent(key, (k, v) -> folder.apply(v, value));
+            map.computeIfAbsent(key, k -> value);
+        });
+        return map.values();
+	}
+}

+ 11 - 1
src/main/lombok/org/leumasjaffe/recipe/view/CardPanel.java

@@ -2,6 +2,8 @@ package org.leumasjaffe.recipe.view;
 
 
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
+import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Card;
 import org.leumasjaffe.recipe.model.Card;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.leumasjaffe.recipe.model.Rest;
 import org.leumasjaffe.recipe.model.Rest;
@@ -11,13 +13,15 @@ import org.jdesktop.swingx.VerticalLayout;
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 public class CardPanel extends JPanel {
 public class CardPanel extends JPanel {
 	private int steps = 0;
 	private int steps = 0;
-	
+	private final ForwardingObservableListener<Card> listener = new ForwardingObservableListener<>();
+
 	public CardPanel(final Card card) {		
 	public CardPanel(final Card card) {		
 		setLayout(new VerticalLayout(5));
 		setLayout(new VerticalLayout(5));
 		
 		
 		card.getPreparation().ifPresent(this::addPrep);
 		card.getPreparation().ifPresent(this::addPrep);
 		card.getCooking().forEach(this::addStep);
 		card.getCooking().forEach(this::addStep);
 		card.getRest().ifPresent(this::addRest);
 		card.getRest().ifPresent(this::addRest);
+		listener.setObserved(card, card.getCooking());
 	}
 	}
 	
 	
 	void addPrep(final Preparation step) {
 	void addPrep(final Preparation step) {
@@ -31,5 +35,11 @@ public class CardPanel extends JPanel {
 	void addRest(final Rest rest) {
 	void addRest(final Rest rest) {
 		add(new RestPanel(rest));
 		add(new RestPanel(rest));
 	}
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
 
 
 }
 }

+ 19 - 5
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -11,6 +11,8 @@ import java.util.Locale;
 import javax.swing.event.DocumentListener;
 import javax.swing.event.DocumentListener;
 import javax.swing.text.NumberFormatter;
 import javax.swing.text.NumberFormatter;
 
 
+import org.leumasjaffe.observer.ObservableController;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
 
 
 import javax.swing.JFormattedTextField;
 import javax.swing.JFormattedTextField;
@@ -19,10 +21,9 @@ import javax.swing.JLabel;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+	ObservableController<JTextField, Ingredient> controller;
 	private JTextField txtName;
 	private JTextField txtName;
-	private JTextField txtUnit;
-	private JTextField txtPreparation;
-	
+		
 	public IngredientPanel(final Ingredient ingredient) {
 	public IngredientPanel(final Ingredient ingredient) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0};
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0};
@@ -63,7 +64,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		add(txtAmount, gbc_txtAmount);
 		add(txtAmount, gbc_txtAmount);
 		txtAmount.setColumns(4);
 		txtAmount.setColumns(4);
 		
 		
-		txtUnit = new JTextField(ingredient.getAmount().unitName());
+		JTextField txtUnit = new JTextField(ingredient.getAmount().unitName());
 		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
 		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
 		gbc_txtUnit.insets = new Insets(0, 0, 0, 5);
 		gbc_txtUnit.insets = new Insets(0, 0, 0, 5);
@@ -74,7 +75,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		add(txtUnit, gbc_txtUnit);
 		add(txtUnit, gbc_txtUnit);
 		txtUnit.setColumns(6);
 		txtUnit.setColumns(6);
 		
 		
-		txtPreparation = new JTextField(ingredient.getPreparation());
+		JTextField txtPreparation = new JTextField(ingredient.getPreparation());
 		txtPreparation.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtPreparation.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtPreparation = new GridBagConstraints();
 		GridBagConstraints gbc_txtPreparation = new GridBagConstraints();
 		gbc_txtPreparation.anchor = GridBagConstraints.ABOVE_BASELINE;
 		gbc_txtPreparation.anchor = GridBagConstraints.ABOVE_BASELINE;
@@ -83,6 +84,13 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		gbc_txtPreparation.gridy = 0;
 		gbc_txtPreparation.gridy = 0;
 		add(txtPreparation, gbc_txtPreparation);
 		add(txtPreparation, gbc_txtPreparation);
 		txtPreparation.setColumns(10);
 		txtPreparation.setColumns(10);
+		
+		// I technically don't need to listen here as of this change,
+		// but if I ever restore support for it, it will be convenient.
+		controller = new ObservableController<>(txtName,
+				Ingredient::getName, Ingredient::setName,
+				JTextField::setText);
+		controller.setObserved(ingredient);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -94,5 +102,11 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 	public void removeDocumentListener(DocumentListener dl) {
 	public void removeDocumentListener(DocumentListener dl) {
 		this.txtName.getDocument().removeDocumentListener(dl);
 		this.txtName.getDocument().removeDocumentListener(dl);
 	}
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(controller);
+	}
 
 
 }
 }

+ 104 - 0
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java

@@ -0,0 +1,104 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import java.awt.GridBagLayout;
+import javax.swing.JTextField;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import javax.swing.text.NumberFormatter;
+
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Ingredient;
+
+import javax.swing.JFormattedTextField;
+import java.awt.Font;
+import javax.swing.JLabel;
+
+@SuppressWarnings("serial")
+public class IngredientPreparationPanel extends JPanel {
+	
+	private ObservableListener<IngredientPreparationPanel, Ingredient> listener;
+	
+	public IngredientPreparationPanel(final Ingredient ingredient) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel label = new JLabel("\u2022");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.insets = new Insets(0, 0, 0, 5);
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.gridx = 0;
+		gbc_label.gridy = 0;
+		add(label, gbc_label);
+		
+		JTextField txtName = new JTextField(ingredient.getName());
+		txtName.setEditable(false);
+		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtName.insets = new Insets(0, 0, 0, 5);
+		gbc_txtName.gridx = 1;
+		gbc_txtName.gridy = 0;
+		add(txtName, gbc_txtName);
+		txtName.setColumns(10);
+		
+		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
+		fmtDone.setMinimum(0.0);
+		fmtDone.setCommitsOnValidEdit(true);
+		JFormattedTextField txtAmount = new JFormattedTextField(fmtDone);
+		txtAmount.setEditable(false);
+		txtAmount.setValue(ingredient.getAmount().getValue());
+		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
+		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtAmount.insets = new Insets(0, 0, 0, 5);
+		gbc_txtAmount.gridx = 2;
+		gbc_txtAmount.gridy = 0;
+		add(txtAmount, gbc_txtAmount);
+		txtAmount.setColumns(4);
+		
+		JTextField txtUnit = new JTextField(ingredient.getAmount().unitName());
+		txtUnit.setEditable(false);
+		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
+		gbc_txtUnit.insets = new Insets(0, 0, 0, 5);
+		gbc_txtUnit.anchor = GridBagConstraints.ABOVE_BASELINE;
+		gbc_txtUnit.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtUnit.gridx = 3;
+		gbc_txtUnit.gridy = 0;
+		add(txtUnit, gbc_txtUnit);
+		txtUnit.setColumns(6);
+		
+		JTextField txtPreparation = new JTextField(ingredient.getPreparation());
+		txtPreparation.setEditable(false);
+		txtPreparation.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtPreparation = new GridBagConstraints();
+		gbc_txtPreparation.anchor = GridBagConstraints.ABOVE_BASELINE;
+		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtPreparation.gridx = 4;
+		gbc_txtPreparation.gridy = 0;
+		add(txtPreparation, gbc_txtPreparation);
+		txtPreparation.setColumns(10);
+		
+		listener = new ObservableListener<>(this, (c, t) -> {
+			if (txtName.getText().equals(t.getName())) return;
+			txtName.setText(t.getName());
+		});
+		listener.setObserved(ingredient);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
+
+}

+ 5 - 14
src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java

@@ -1,8 +1,8 @@
 package org.leumasjaffe.recipe.view;
 package org.leumasjaffe.recipe.view;
 
 
 import javax.swing.JPanel;
 import javax.swing.JPanel;
-import javax.swing.event.DocumentListener;
 
 
+import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.leumasjaffe.recipe.model.Preparation;
 
 
 import java.awt.GridBagLayout;
 import java.awt.GridBagLayout;
@@ -11,14 +11,12 @@ import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Insets;
 
 
 import javax.swing.JLabel;
 import javax.swing.JLabel;
-import javax.swing.JTextPane;
 import java.awt.Component;
 import java.awt.Component;
 import javax.swing.Box;
 import javax.swing.Box;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
-public class PreparationPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+public class PreparationPanel extends JPanel {
 	private JLabel lblIndex;
 	private JLabel lblIndex;
-	private JTextPane txtpnInstructions;
 		
 		
 	public PreparationPanel(Preparation step) {
 	public PreparationPanel(Preparation step) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -64,7 +62,8 @@ public class PreparationPanel extends JPanel implements AutoGrowPanel.DocumentLi
 		gbc_lblDuration.gridy = 0;
 		gbc_lblDuration.gridy = 0;
 		panelLeft.add(lblDuration, gbc_lblDuration);
 		panelLeft.add(lblDuration, gbc_lblDuration);
 		
 		
-		AutoGrowPanel panelIngredients = new AutoGrowPanel(IngredientPanel::new, step.getIngredients());
+		JPanel panelIngredients = new JPanel();
+		panelIngredients.setLayout(new VerticalLayout(5));
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
 		gbc_panelIngredients.gridwidth = 3;
 		gbc_panelIngredients.gridwidth = 3;
 		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
 		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
@@ -72,15 +71,7 @@ public class PreparationPanel extends JPanel implements AutoGrowPanel.DocumentLi
 		gbc_panelIngredients.gridx = 0;
 		gbc_panelIngredients.gridx = 0;
 		gbc_panelIngredients.gridy = 1;
 		gbc_panelIngredients.gridy = 1;
 		panelLeft.add(panelIngredients, gbc_panelIngredients);
 		panelLeft.add(panelIngredients, gbc_panelIngredients);
+		step.getIngredients().stream().map(IngredientPreparationPanel::new).forEach(panelIngredients::add);
 	}
 	}
 
 
-	@Override
-	public void addDocumentListener(DocumentListener dl) {
-		this.txtpnInstructions.getDocument().addDocumentListener(dl);
-	}
-
-	@Override
-	public void removeDocumentListener(DocumentListener dl) {
-		this.txtpnInstructions.getDocument().removeDocumentListener(dl);		
-	}
 }
 }

+ 6 - 1
src/main/lombok/org/leumasjaffe/recipe/view/ProductPanel.java

@@ -3,6 +3,7 @@ package org.leumasjaffe.recipe.view;
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JScrollPane;
 
 
+import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.recipe.model.Card;
 import org.leumasjaffe.recipe.model.Card;
 import org.leumasjaffe.recipe.model.Product;
 import org.leumasjaffe.recipe.model.Product;
 import org.jdesktop.swingx.VerticalLayout;
 import org.jdesktop.swingx.VerticalLayout;
@@ -13,6 +14,8 @@ import javax.swing.JSeparator;
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 public class ProductPanel extends JScrollPane {
 public class ProductPanel extends JScrollPane {
 	private JPanel panelViewPort;
 	private JPanel panelViewPort;
+	private final ForwardingObservableListener<Product> listener = new ForwardingObservableListener<>();
+
 	public ProductPanel(Product product) {
 	public ProductPanel(Product product) {
 		JPanel panelColumnHeader = new JPanel();
 		JPanel panelColumnHeader = new JPanel();
 		setColumnHeaderView(panelColumnHeader);
 		setColumnHeaderView(panelColumnHeader);
@@ -23,10 +26,12 @@ public class ProductPanel extends JScrollPane {
 		panelViewPort = new JPanel();
 		panelViewPort = new JPanel();
 		setViewportView(panelViewPort);
 		setViewportView(panelViewPort);
 		panelViewPort.setLayout(new VerticalLayout(5));
 		panelViewPort.setLayout(new VerticalLayout(5));
-		
+				
 		for (final Card card : product.getCards()) {
 		for (final Card card : product.getCards()) {
 			panelViewPort.add(new CardPanel(card));
 			panelViewPort.add(new CardPanel(card));
 			panelViewPort.add(new JSeparator());
 			panelViewPort.add(new JSeparator());
 		}
 		}
+		
+		listener.setObserved(product, product.getCards());
 	}
 	}
 }
 }

+ 31 - 13
src/main/lombok/org/leumasjaffe/recipe/view/ProductSummaryPanel.java

@@ -2,38 +2,56 @@ package org.leumasjaffe.recipe.view;
 
 
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Product;
 import org.leumasjaffe.recipe.model.Product;
+
 import java.awt.GridBagLayout;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
 import java.awt.Insets;
-import java.util.ArrayList;
 
 
 import javax.swing.JLabel;
 import javax.swing.JLabel;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagConstraints;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 public class ProductSummaryPanel extends JPanel {
 public class ProductSummaryPanel extends JPanel {
+	ObservableListener<JPanel, Product> listener;
+	
 	public ProductSummaryPanel(final Product product) {
 	public ProductSummaryPanel(final Product product) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
-		gridBagLayout.rowHeights = new int[]{0, 0};
-		gridBagLayout.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
-		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		setLayout(gridBagLayout);
 		
 		
 		JLabel lblProductName = new JLabel(product.getName());
 		JLabel lblProductName = new JLabel(product.getName());
 		GridBagConstraints gbc_lblProductName = new GridBagConstraints();
 		GridBagConstraints gbc_lblProductName = new GridBagConstraints();
+		gbc_lblProductName.insets = new Insets(0, 0, 5, 5);
 		gbc_lblProductName.gridx = 0;
 		gbc_lblProductName.gridx = 0;
 		gbc_lblProductName.gridy = 0;
 		gbc_lblProductName.gridy = 0;
 		add(lblProductName, gbc_lblProductName);
 		add(lblProductName, gbc_lblProductName);
+				
+		JPanel panel = new JPanel();
+		panel.setLayout(new VerticalLayout());
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.gridwidth = 2;
+		gbc_panel.insets = new Insets(0, 0, 0, 5);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 1;
+		add(panel, gbc_panel);
 		
 		
-		AutoGrowPanel panelIngredients = new AutoGrowPanel(IngredientPanel::new,
-				new ArrayList<>(product.getIngredients()));
-		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
-		gbc_panelIngredients.gridwidth = 2;
-		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
-		gbc_panelIngredients.fill = GridBagConstraints.BOTH;
-		gbc_panelIngredients.gridx = 0;
-		gbc_panelIngredients.gridy = 1;
-		add(panelIngredients, gbc_panelIngredients);
+		listener = new ObservableListener<>(panel, (c, t) -> {
+			c.removeAll();
+			product.getIngredientsAsStream().map(SummaryIngredientPanel::new).forEach(c::add);
+		});
+		listener.setObserved(product);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
 	}
 	}
 }
 }

+ 10 - 0
src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java

@@ -3,6 +3,8 @@ package org.leumasjaffe.recipe.view;
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 import javax.swing.event.DocumentListener;
 import javax.swing.event.DocumentListener;
 
 
+import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Step;
 import org.leumasjaffe.recipe.model.Step;
 
 
 import java.awt.GridBagLayout;
 import java.awt.GridBagLayout;
@@ -20,6 +22,7 @@ import java.awt.Dimension;
 public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 	private JLabel lblIndex;
 	private JLabel lblIndex;
 	private JTextPane txtpnInstructions;
 	private JTextPane txtpnInstructions;
+	private final ForwardingObservableListener<Step> listener = new ForwardingObservableListener<>();
 		
 		
 	public StepPanel(int zeroIndex, Step step) {
 	public StepPanel(int zeroIndex, Step step) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -84,6 +87,7 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 		add(txtpnInstructions, gbc_txtpnInstructions);
 		add(txtpnInstructions, gbc_txtpnInstructions);
 		
 		
 		setListPosition(zeroIndex);
 		setListPosition(zeroIndex);
+		listener.setObserved(step, step.getIngredients());
 	}
 	}
 
 
 	@Override
 	@Override
@@ -101,4 +105,10 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 		this.lblIndex.setText("Step " + Integer.toString(zeroIndex + 1));
 		this.lblIndex.setText("Step " + Integer.toString(zeroIndex + 1));
 		repaint();
 		repaint();
 	}
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
 }
 }

+ 91 - 0
src/main/lombok/org/leumasjaffe/recipe/view/SummaryIngredientPanel.java

@@ -0,0 +1,91 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import java.awt.GridBagLayout;
+import javax.swing.JTextField;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import javax.swing.text.NumberFormatter;
+
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Ingredient;
+
+import javax.swing.JFormattedTextField;
+import java.awt.Font;
+import javax.swing.JLabel;
+
+@SuppressWarnings("serial")
+public class SummaryIngredientPanel extends JPanel {
+	private ObservableListener<SummaryIngredientPanel, Ingredient> listener;
+	
+	public SummaryIngredientPanel(final Ingredient ingredient) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel label = new JLabel("\u2022");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.insets = new Insets(0, 0, 0, 5);
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.gridx = 0;
+		gbc_label.gridy = 0;
+		add(label, gbc_label);
+		
+		JTextField txtName = new JTextField(ingredient.getName());
+		txtName.setEditable(false);
+		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtName.insets = new Insets(0, 0, 0, 5);
+		gbc_txtName.gridx = 1;
+		gbc_txtName.gridy = 0;
+		add(txtName, gbc_txtName);
+		txtName.setColumns(10);
+		
+		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
+		fmtDone.setMinimum(0.0);
+		fmtDone.setCommitsOnValidEdit(true);
+		JFormattedTextField txtAmount = new JFormattedTextField(fmtDone);
+		txtAmount.setEditable(false);
+		txtAmount.setValue(ingredient.getAmount().getValue());
+		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
+		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtAmount.insets = new Insets(0, 0, 0, 5);
+		gbc_txtAmount.gridx = 2;
+		gbc_txtAmount.gridy = 0;
+		add(txtAmount, gbc_txtAmount);
+		txtAmount.setColumns(4);
+		
+		JTextField txtUnit = new JTextField(ingredient.getAmount().unitName());
+		txtUnit.setEditable(false);
+		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
+		gbc_txtUnit.anchor = GridBagConstraints.ABOVE_BASELINE;
+		gbc_txtUnit.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtUnit.gridx = 3;
+		gbc_txtUnit.gridy = 0;
+		add(txtUnit, gbc_txtUnit);
+		txtUnit.setColumns(6);
+		
+		listener = new ObservableListener<>(this, (c, t) -> {
+			if (txtName.getText().equals(t.getName())) return;
+			txtName.setText(t.getName());
+		});
+		listener.setObserved(ingredient);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
+
+}

+ 0 - 19
src/test/resources/example.json

@@ -10,25 +10,6 @@
           "dependsOn": [],
           "dependsOn": [],
           "vessel": "",
           "vessel": "",
           "preparation": {
           "preparation": {
-            "ingredients": [
-              {
-                "name": "onion",
-                "preparation": "chopped",
-                "amount": "100 g"
-              },
-              {
-                "name": "onion",
-                "amount": "1"
-              },
-              {
-              	"name": "oil",
-              	"amount": "1 Tbsp"
-            	},
-              {
-              	"name": "oil",
-              	"amount": "1 Tbsp"
-            	}
-            ],
             "duration": {
             "duration": {
               "displayAs": "MINUTES",
               "displayAs": "MINUTES",
               "approximate": true,
               "approximate": true,