소스 검색

Merge branch 'feat/input-duration'

* feat/input-duration:
  Add coverage to phase-panel test.
  Make some better tests.
  Add duration hookups, minimally alter tests to pass.
  Get better ObservableController from v0.6.0
  Add ObservableController to IngredientPanel for Amounts
  Switch all ingredient panels to use AmountFormatter instead of two separate fields.
  Remove unneeded dependency.
  Change DurationPanel to use the new Formatter. Make CollatedDurationPanel's children not editable.
  Add formatters for Duration and Amount in anticipation of using them.
  Add string ctor to Duration, as well as the test cases to cover it.
Sam Jaffe 5 년 전
부모
커밋
7db8dfb3e9
28개의 변경된 파일481개의 추가작업 그리고 240개의 파일을 삭제
  1. 1 6
      pom.xml
  2. 25 1
      src/main/lombok/org/leumasjaffe/recipe/model/Duration.java
  3. 5 2
      src/main/lombok/org/leumasjaffe/recipe/model/Rest.java
  4. 11 6
      src/main/lombok/org/leumasjaffe/recipe/view/CollatedDurationPanel.java
  5. 18 8
      src/main/lombok/org/leumasjaffe/recipe/view/DurationPanel.java
  6. 22 5
      src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java
  7. 28 36
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java
  8. 21 34
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java
  9. 9 1
      src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java
  10. 7 4
      src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java
  11. 0 1
      src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java
  12. 28 11
      src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java
  13. 11 1
      src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java
  14. 34 0
      src/main/lombok/org/leumasjaffe/recipe/view/formatter/AmountFormatter.java
  15. 49 0
      src/main/lombok/org/leumasjaffe/recipe/view/formatter/DurationFormatter.java
  16. 6 26
      src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java
  17. 1 2
      src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java
  18. 28 0
      src/test/java/org/leumasjaffe/recipe/model/DurationTest.java
  19. 1 1
      src/test/java/org/leumasjaffe/recipe/model/PhaseTest.java
  20. 7 24
      src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java
  21. 8 4
      src/test/java/org/leumasjaffe/recipe/view/IngredientPreparationPanelTest.java
  22. 26 1
      src/test/java/org/leumasjaffe/recipe/view/PhasePanelTest.java
  23. 7 20
      src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java
  24. 25 13
      src/test/java/org/leumasjaffe/recipe/view/RestPanelTest.java
  25. 40 0
      src/test/java/org/leumasjaffe/recipe/view/formatter/AmountFormatterTest.java
  26. 56 0
      src/test/java/org/leumasjaffe/recipe/view/formatter/DurationFormatterTest.java
  27. 1 3
      src/test/java/org/leumasjaffe/recipe/view/summary/IngredientPanelTest.java
  28. 6 30
      src/test/resources/example.json

+ 1 - 6
pom.xml

@@ -75,7 +75,7 @@
     <dependency>
     <dependency>
       <groupId>org.leumasjaffe</groupId>
       <groupId>org.leumasjaffe</groupId>
       <artifactId>observer</artifactId>
       <artifactId>observer</artifactId>
-      <version>0.5.1</version>
+      <version>0.6.0</version>
     </dependency>
     </dependency>
     <dependency>
     <dependency>
       <groupId>org.leumasjaffe</groupId>
       <groupId>org.leumasjaffe</groupId>
@@ -114,11 +114,6 @@
       <artifactId>hamcrest-all</artifactId>
       <artifactId>hamcrest-all</artifactId>
       <version>1.3</version>
       <version>1.3</version>
     </dependency>
     </dependency>
-    <dependency>
-      <groupId>org.leumasjaffe</groupId>
-      <artifactId>container</artifactId>
-      <version>0.3.0</version>
-    </dependency>
     <dependency>
     <dependency>
       <groupId>org.junit.platform</groupId>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-runner</artifactId>
       <artifactId>junit-platform-runner</artifactId>

+ 25 - 1
src/main/lombok/org/leumasjaffe/recipe/model/Duration.java

@@ -1,5 +1,11 @@
 package org.leumasjaffe.recipe.model;
 package org.leumasjaffe.recipe.model;
 
 
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.EqualsAndHashCode;
@@ -24,6 +30,24 @@ public class Duration {
 	int minSeconds = 0;
 	int minSeconds = 0;
 	int maxSeconds = 0;
 	int maxSeconds = 0;
 	
 	
+	@JsonCreator
+	public Duration(final String serial) {
+		final String[] tokens = serial.split(" ");
+		final boolean isRange = tokens.length == 4;
+		
+		final Optional<Display> rv = Stream.of(Display.values())
+				.filter(v -> v.abbreviation.equals(tokens[isRange ? 3 : 1])).findFirst();
+		
+		displayAs = rv.orElseThrow(() -> new IllegalArgumentException("Unknown time measure: " + tokens[1]));
+
+		if (isRange) {
+			minSeconds = (int) (Float.parseFloat(tokens[0]) * displayAs.inSeconds);
+			maxSeconds = (int) (Float.parseFloat(tokens[2]) * displayAs.inSeconds);
+		} else {
+			maxSeconds = (int) (Float.parseFloat(tokens[0]) * displayAs.inSeconds);
+		}
+	}
+	
 	public Duration plus(Duration rhs) {
 	public Duration plus(Duration rhs) {
 		final Display newDisplayAs = displayAs.ordinal() < rhs.displayAs.ordinal() ?
 		final Display newDisplayAs = displayAs.ordinal() < rhs.displayAs.ordinal() ?
 				displayAs : rhs.displayAs;
 				displayAs : rhs.displayAs;
@@ -50,7 +74,7 @@ public class Duration {
 		return this;
 		return this;
 	}
 	}
 
 
-	@Override
+	@Override @JsonValue
 	public String toString() {
 	public String toString() {
 		StringBuilder build = new StringBuilder();
 		StringBuilder build = new StringBuilder();
 		if (minSeconds != 0) {
 		if (minSeconds != 0) {

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

@@ -1,14 +1,17 @@
 package org.leumasjaffe.recipe.model;
 package org.leumasjaffe.recipe.model;
 
 
+import org.leumasjaffe.observer.Observable;
+
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.NoArgsConstructor;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.FieldDefaults;
 
 
-@Data @AllArgsConstructor @NoArgsConstructor
-public class Rest {
+@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper=false)
+public class Rest extends Observable.Instance {
 	@AllArgsConstructor @Getter @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	@AllArgsConstructor @Getter @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	public enum Where {
 	public enum Where {
 		FREEZER("in the freezer"), REFRIGERATOR("in the refrigerator"),
 		FREEZER("in the freezer"), REFRIGERATOR("in the refrigerator"),

+ 11 - 6
src/main/lombok/org/leumasjaffe/recipe/view/CollatedDurationPanel.java

@@ -6,7 +6,6 @@ import javax.swing.SwingConstants;
 
 
 import org.jdesktop.swingx.HorizontalLayout;
 import org.jdesktop.swingx.HorizontalLayout;
 import org.leumasjaffe.recipe.model.CollatedDuration;
 import org.leumasjaffe.recipe.model.CollatedDuration;
-import org.leumasjaffe.recipe.model.Duration;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.FieldDefaults;
@@ -18,20 +17,26 @@ public class CollatedDurationPanel extends JPanel {
 	DurationPanel panelCookingTime;
 	DurationPanel panelCookingTime;
 	DurationPanel panelTotalTime;
 	DurationPanel panelTotalTime;
 	
 	
-	public CollatedDurationPanel(final CollatedDuration duration) {
+	public CollatedDurationPanel() {
 		setLayout(new HorizontalLayout(5));
 		setLayout(new HorizontalLayout(5));
 		
 		
-		panelPrepTime = new DurationPanel("Prep", Duration.ZERO);
+		panelPrepTime = new DurationPanel("Prep");
+		panelPrepTime.setEditable(false);
 		add(panelPrepTime);
 		add(panelPrepTime);
 		add(new JSeparator(SwingConstants.VERTICAL));
 		add(new JSeparator(SwingConstants.VERTICAL));
 		
 		
-		panelCookingTime = new DurationPanel("Cooking", Duration.ZERO);
+		panelCookingTime = new DurationPanel("Cooking");
+		panelCookingTime.setEditable(false);
 		add(panelCookingTime);
 		add(panelCookingTime);
 		add(new JSeparator(SwingConstants.VERTICAL));
 		add(new JSeparator(SwingConstants.VERTICAL));
 		
 		
-		panelTotalTime = new DurationPanel("Total", Duration.ZERO);
+		panelTotalTime = new DurationPanel("Total");
+		panelTotalTime.setEditable(false);
 		add(panelTotalTime);
 		add(panelTotalTime);
-		
+	}
+	
+	public CollatedDurationPanel(final CollatedDuration duration) {
+		this();
 		setModel(duration);
 		setModel(duration);
 	}
 	}
 	
 	

+ 18 - 8
src/main/lombok/org/leumasjaffe/recipe/view/DurationPanel.java

@@ -4,14 +4,20 @@ import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
 import java.awt.Insets;
 
 
+import javax.swing.JFormattedTextField;
 import javax.swing.JLabel;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Duration;
+import org.leumasjaffe.recipe.view.formatter.DurationFormatter;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 public class DurationPanel extends JPanel {
 public class DurationPanel extends JPanel {
-	private JLabel lblTime;
+	JFormattedTextField txtTime;
+	
+	/**
+	 * @wbp.parser.constructor
+	 */
 	public DurationPanel(String name) {
 	public DurationPanel(String name) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
@@ -27,19 +33,23 @@ public class DurationPanel extends JPanel {
 		gbc_lblName.gridy = 0;
 		gbc_lblName.gridy = 0;
 		add(lblName, gbc_lblName);
 		add(lblName, gbc_lblName);
 		
 		
-		lblTime = new JLabel();
-		GridBagConstraints gbc_lblTime = new GridBagConstraints();
-		gbc_lblTime.gridx = 1;
-		gbc_lblTime.gridy = 0;
-		add(lblTime, gbc_lblTime);
+		txtTime = new JFormattedTextField(new DurationFormatter());
+		GridBagConstraints gbc_txtTime = new GridBagConstraints();
+		gbc_txtTime.gridx = 1;
+		gbc_txtTime.gridy = 0;
+		add(txtTime, gbc_txtTime);
 	}
 	}
 
 
-	public DurationPanel(String name, Duration duration) {
+	public DurationPanel(String name, final Duration duration) {
 		this(name);
 		this(name);
 		setModel(duration);
 		setModel(duration);
 	}
 	}
 
 
 	public void setModel(final Duration duration) {
 	public void setModel(final Duration duration) {
-		lblTime.setText(duration.toString());
+		txtTime.setValue(duration);
+	}
+
+	public void setEditable(boolean b) {
+		txtTime.setEditable(b);
 	}
 	}
 }
 }

+ 22 - 5
src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java

@@ -4,6 +4,7 @@ import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JScrollPane;
 
 
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.Element;
@@ -28,10 +29,13 @@ import javax.swing.Box;
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class ElementPanel extends JScrollPane {
 public class ElementPanel extends JScrollPane {
-	JPanel panelViewPort;
+	ObservableListener<CollatedDurationPanel, Element> durationListener;
 	ForwardingObservableListener<Element> listener = new ForwardingObservableListener<>();
 	ForwardingObservableListener<Element> listener = new ForwardingObservableListener<>();
 
 
-	public ElementPanel(Element element) {
+	JLabel lblName;
+	JPanel panelViewPort;
+
+	public ElementPanel() {
 		setPreferredSize(new Dimension(500, 450));
 		setPreferredSize(new Dimension(500, 450));
 
 
 		setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
 		setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
@@ -46,7 +50,7 @@ public class ElementPanel extends JScrollPane {
 		gbl_panelColumnHeader.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		gbl_panelColumnHeader.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		panelColumnHeader.setLayout(gbl_panelColumnHeader);
 		panelColumnHeader.setLayout(gbl_panelColumnHeader);
 		
 		
-		JLabel lblName = new JLabel(element.getName());
+		lblName = new JLabel();
 		GridBagConstraints gbc_lblName = new GridBagConstraints();
 		GridBagConstraints gbc_lblName = new GridBagConstraints();
 		gbc_lblName.insets = new Insets(0, 0, 0, 5);
 		gbc_lblName.insets = new Insets(0, 0, 0, 5);
 		gbc_lblName.gridx = 0;
 		gbc_lblName.gridx = 0;
@@ -60,8 +64,7 @@ public class ElementPanel extends JScrollPane {
 		gbc_horizontalGlue.gridy = 0;
 		gbc_horizontalGlue.gridy = 0;
 		panelColumnHeader.add(horizontalGlue, gbc_horizontalGlue);
 		panelColumnHeader.add(horizontalGlue, gbc_horizontalGlue);
 		
 		
-		CollatedDurationPanel panelDuration =
-				new CollatedDurationPanel(element.getCollatedDuration());
+		CollatedDurationPanel panelDuration = new CollatedDurationPanel();
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridy = 0;
 		gbc_panelDuration.gridy = 0;
@@ -71,12 +74,26 @@ public class ElementPanel extends JScrollPane {
 		setViewportView(panelViewPort);
 		setViewportView(panelViewPort);
 		panelViewPort.setLayout(new VerticalLayout(5));
 		panelViewPort.setLayout(new VerticalLayout(5));
 				
 				
+		durationListener = new ObservableListener<>(panelDuration,
+				(c, v) -> c.setModel(v.getCollatedDuration()));
+	}
+	
+	public ElementPanel(final Element element) {
+		this();
+		setModel(element);
+	}
+	
+	public void setModel(final Element element) {
+		lblName.setText(element.getName());
+		
+		panelViewPort.removeAll();
 		for (final Phase phase : element.getPhases()) {
 		for (final Phase phase : element.getPhases()) {
 			panelViewPort.add(new PhasePanel(phase));
 			panelViewPort.add(new PhasePanel(phase));
 			panelViewPort.add(new JSeparator());
 			panelViewPort.add(new JSeparator());
 		}
 		}
 		
 		
 		listener.setObserved(element, element.getPhases());
 		listener.setObserved(element, element.getPhases());
+		durationListener.setObserved(element);
 	}
 	}
 	
 	
 	@Override
 	@Override

+ 28 - 36
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -5,15 +5,14 @@ import java.awt.GridBagLayout;
 import javax.swing.JTextField;
 import javax.swing.JTextField;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Insets;
-import java.text.NumberFormat;
-import java.util.Locale;
 
 
 import javax.swing.event.DocumentListener;
 import javax.swing.event.DocumentListener;
-import javax.swing.text.NumberFormatter;
 
 
 import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableController;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Getter;
@@ -26,18 +25,18 @@ import javax.swing.JLabel;
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
-	ObservableController<JTextField, Ingredient> nameController;
-	ObservableController<JTextField, Ingredient> preparationController;
+	ObservableListener<JTextField, Ingredient> nameController;
+	ObservableListener<JFormattedTextField, Ingredient> amountController;
+	ObservableListener<JTextField, Ingredient> preparationController;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
-	@Getter(AccessLevel.PACKAGE) JTextField txtUnit;
 	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
 	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
 		
 		
-	public IngredientPanel(final Ingredient ingredient) {
+	public IngredientPanel() {
 		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};
 		gridBagLayout.rowHeights = new int[]{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.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		setLayout(gridBagLayout);
 		
 		
@@ -49,7 +48,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		gbc_label.gridy = 0;
 		gbc_label.gridy = 0;
 		add(label, gbc_label);
 		add(label, gbc_label);
 		
 		
-		txtName = new JTextField(ingredient.getName());
+		txtName = new JTextField();
 		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
 		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
@@ -59,11 +58,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		add(txtName, gbc_txtName);
 		add(txtName, gbc_txtName);
 		txtName.setColumns(10);
 		txtName.setColumns(10);
 		
 		
-		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
-		fmtDone.setMinimum(0.0);
-		fmtDone.setCommitsOnValidEdit(true);
-		txtAmount = new JFormattedTextField(fmtDone);
-		txtAmount.setValue(ingredient.getAmount().getValue());
+		txtAmount = new JFormattedTextField(new AmountFormatter());
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
@@ -71,42 +66,39 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridy = 0;
 		gbc_txtAmount.gridy = 0;
 		add(txtAmount, gbc_txtAmount);
 		add(txtAmount, gbc_txtAmount);
-		txtAmount.setColumns(4);
-		
-		txtUnit = new JTextField(ingredient.getAmount().unitName());
-		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);
+		txtAmount.setColumns(11);
 		
 		
-		txtPreparation = new JTextField(ingredient.getPreparation());
+		txtPreparation = new JTextField();
 		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;
 		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
-		gbc_txtPreparation.gridx = 4;
+		gbc_txtPreparation.gridx = 3;
 		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,
 		// I technically don't need to listen here as of this change,
 		// but if I ever restore support for it, it will be convenient.
 		// but if I ever restore support for it, it will be convenient.
-		nameController = new ObservableController<>(txtName,
-				Ingredient::getName, Ingredient::setName,
-				JTextField::setText);
-		preparationController = new ObservableController<>(txtPreparation,
-				Ingredient::getPreparation, Ingredient::setPreparation,
-				JTextField::setText);
+		nameController = ObservableController.from(txtName,
+				Ingredient::getName, Ingredient::setName);
+		amountController = ObservableController.from(txtAmount,
+				Ingredient::getAmount, Ingredient::setAmount);
+		preparationController = ObservableController.from(txtPreparation,
+				Ingredient::getPreparation, Ingredient::setPreparation);
+	}
 
 
+	public IngredientPanel(final Ingredient ingredient) {
+		this();
+		setModel(ingredient);
+	}
+	
+	public void setModel(final Ingredient ingredient) {
 		nameController.setObserved(ingredient);
 		nameController.setObserved(ingredient);
+		amountController.setObserved(ingredient);
 		preparationController.setObserved(ingredient);
 		preparationController.setObserved(ingredient);
 	}
 	}
-
+	
 	@Override
 	@Override
 	public void addDocumentListener(DocumentListener dl) {
 	public void addDocumentListener(DocumentListener dl) {
 		this.txtName.getDocument().addDocumentListener(dl);
 		this.txtName.getDocument().addDocumentListener(dl);

+ 21 - 34
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java

@@ -5,14 +5,11 @@ import java.awt.GridBagLayout;
 import javax.swing.JTextField;
 import javax.swing.JTextField;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 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.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Getter;
@@ -29,14 +26,13 @@ public class IngredientPreparationPanel extends JPanel {
 	ObservableListener<IngredientPreparationPanel, Ingredient> listener;
 	ObservableListener<IngredientPreparationPanel, Ingredient> listener;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
-	@Getter(AccessLevel.PACKAGE) JTextField txtUnit;
 	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
 	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
 	
 	
-	public IngredientPreparationPanel(final Ingredient ingredient) {
+	public IngredientPreparationPanel() {
 		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};
 		gridBagLayout.rowHeights = new int[]{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.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		setLayout(gridBagLayout);
 		
 		
@@ -48,7 +44,7 @@ public class IngredientPreparationPanel extends JPanel {
 		gbc_label.gridy = 0;
 		gbc_label.gridy = 0;
 		add(label, gbc_label);
 		add(label, gbc_label);
 		
 		
-		txtName = new JTextField(ingredient.getName());
+		txtName = new JTextField();
 		txtName.setEditable(false);
 		txtName.setEditable(false);
 		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
@@ -59,12 +55,8 @@ public class IngredientPreparationPanel extends JPanel {
 		add(txtName, gbc_txtName);
 		add(txtName, gbc_txtName);
 		txtName.setColumns(10);
 		txtName.setColumns(10);
 		
 		
-		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
-		fmtDone.setMinimum(0.0);
-		fmtDone.setCommitsOnValidEdit(true);
-		txtAmount = new JFormattedTextField(fmtDone);
+		txtAmount = new JFormattedTextField(new AmountFormatter());
 		txtAmount.setEditable(false);
 		txtAmount.setEditable(false);
-		txtAmount.setValue(ingredient.getAmount().getValue());
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
@@ -72,37 +64,32 @@ public class IngredientPreparationPanel extends JPanel {
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridy = 0;
 		gbc_txtAmount.gridy = 0;
 		add(txtAmount, gbc_txtAmount);
 		add(txtAmount, gbc_txtAmount);
-		txtAmount.setColumns(4);
+		txtAmount.setColumns(11);
 		
 		
-		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);
-		
-		txtPreparation = new JTextField(ingredient.getPreparation());
+		txtPreparation = new JTextField();
 		txtPreparation.setEditable(false);
 		txtPreparation.setEditable(false);
 		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;
 		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
-		gbc_txtPreparation.gridx = 4;
+		gbc_txtPreparation.gridx = 3;
 		gbc_txtPreparation.gridy = 0;
 		gbc_txtPreparation.gridy = 0;
 		add(txtPreparation, gbc_txtPreparation);
 		add(txtPreparation, gbc_txtPreparation);
 		txtPreparation.setColumns(10);
 		txtPreparation.setColumns(10);
 		
 		
-		listener = new ObservableListener<>(this, (c, t) -> {
-			txtName.setText(t.getName());
-			txtAmount.setValue(t.getAmount().getValue());
-			txtUnit.setText(t.getAmount().unitName());
-			txtPreparation.setText(t.getPreparation());
+		listener = new ObservableListener<>(this, (c, v) -> {
+			c.txtName.setText(v.getName());
+			c.txtAmount.setValue(v.getAmount());
+			c.txtPreparation.setText(v.getPreparation());
 		});
 		});
+	}
+	
+	public IngredientPreparationPanel(final Ingredient ingredient) {
+		this();
+		setModel(ingredient);
+	}
+	
+	public void setModel(final Ingredient ingredient) {
 		listener.setObserved(ingredient);
 		listener.setObserved(ingredient);
 	}
 	}
 	
 	

+ 9 - 1
src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java

@@ -1,8 +1,12 @@
 package org.leumasjaffe.recipe.view;
 package org.leumasjaffe.recipe.view;
 
 
+import java.util.ArrayList;
+import java.util.List;
+
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.leumasjaffe.recipe.model.Preparation;
@@ -27,7 +31,11 @@ public class PhasePanel extends JPanel {
 		phase.getPreparation().ifPresent(this::addPrep);
 		phase.getPreparation().ifPresent(this::addPrep);
 		phase.getCooking().forEach(this::addStep);
 		phase.getCooking().forEach(this::addStep);
 		phase.getRest().ifPresent(this::addRest);
 		phase.getRest().ifPresent(this::addRest);
-		listener.setObserved(phase, phase.getCooking());
+
+		List<Observable> children = new ArrayList<>(phase.getCooking());
+		phase.getPreparation().ifPresent(children::add);
+		phase.getRest().ifPresent(children::add);
+		listener.setObserved(phase, children);
 	}
 	}
 	
 	
 	void addPrep(final Preparation step) {
 	void addPrep(final Preparation step) {

+ 7 - 4
src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java

@@ -3,6 +3,7 @@ package org.leumasjaffe.recipe.view;
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
 import org.jdesktop.swingx.VerticalLayout;
 import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Duration;
@@ -20,15 +21,15 @@ import java.awt.Insets;
 import javax.swing.JLabel;
 import javax.swing.JLabel;
 import java.awt.Component;
 import java.awt.Component;
 import javax.swing.Box;
 import javax.swing.Box;
+import javax.swing.JFormattedTextField;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE)
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class PreparationPanel extends JPanel {
 public class PreparationPanel extends JPanel {
 	ReplaceChildrenController<Preparation, Ingredient> controller;
 	ReplaceChildrenController<Preparation, Ingredient> controller;
 	ObservableListener<JPanel, Preparation> childListener;
 	ObservableListener<JPanel, Preparation> childListener;
+	ObservableListener<JFormattedTextField, Preparation> durationListener;
 	
 	
-	DurationPanel panelDuration;
-		
 	public PreparationPanel() {
 	public PreparationPanel() {
 		controller = new ReplaceChildrenController<>(Preparation::getIngredients,
 		controller = new ReplaceChildrenController<>(Preparation::getIngredients,
 				IngredientPreparationPanel::new);
 				IngredientPreparationPanel::new);
@@ -69,7 +70,7 @@ public class PreparationPanel extends JPanel {
 		gbc_horizontalGlue.gridy = 0;
 		gbc_horizontalGlue.gridy = 0;
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		
 		
-		panelDuration = new DurationPanel("Requires", Duration.ZERO);
+		DurationPanel panelDuration = new DurationPanel("Requires", Duration.ZERO);
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.insets = new Insets(0, 0, 5, 0);
 		gbc_panelDuration.insets = new Insets(0, 0, 5, 0);
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridx = 2;
@@ -89,6 +90,8 @@ public class PreparationPanel extends JPanel {
 		// This indirection allows for testing of controller
 		// This indirection allows for testing of controller
 		childListener = new ObservableListener<>(panelIngredients,
 		childListener = new ObservableListener<>(panelIngredients,
 				(c, v) -> controller.accept(c, v));
 				(c, v) -> controller.accept(c, v));
+		durationListener = ObservableController.from(panelDuration.txtTime,
+				Preparation::getDuration, Preparation::setDuration);
 	}
 	}
 	
 	
 	public PreparationPanel(final Preparation preparation) {
 	public PreparationPanel(final Preparation preparation) {
@@ -97,7 +100,7 @@ public class PreparationPanel extends JPanel {
 	}
 	}
 	
 	
 	public void setModel(final Preparation preparation) {
 	public void setModel(final Preparation preparation) {
-		panelDuration.setModel(preparation.getDuration());
+		durationListener.setObserved(preparation);
 		childListener.setObserved(preparation);
 		childListener.setObserved(preparation);
 	}
 	}
 
 

+ 0 - 1
src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java

@@ -5,7 +5,6 @@ import javax.swing.JSplitPane;
 import org.jdesktop.swingx.VerticalLayout;
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
-import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.summary.SummaryPanel;
 import org.leumasjaffe.recipe.view.summary.SummaryPanel;
 
 

+ 28 - 11
src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java

@@ -2,24 +2,28 @@ package org.leumasjaffe.recipe.view;
 
 
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 
 
+import org.leumasjaffe.observer.ObservableController;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.recipe.model.Rest;
 import org.leumasjaffe.recipe.model.Rest;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
-import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.FieldDefaults;
 
 
 import java.awt.GridBagLayout;
 import java.awt.GridBagLayout;
+
+import javax.swing.JFormattedTextField;
 import javax.swing.JLabel;
 import javax.swing.JLabel;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Insets;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+@FieldDefaults(level=AccessLevel.PRIVATE)
 public class RestPanel extends JPanel {
 public class RestPanel extends JPanel {
-	@Getter(AccessLevel.PACKAGE) JLabel lblLocation;
-	@Getter(AccessLevel.PACKAGE) JLabel lblDuration;
+	ObservableListener<JFormattedTextField, Rest> durationListener;
+
+	JLabel lblLocation;
 
 
-	public RestPanel(Rest rest) {
+	public RestPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -34,18 +38,31 @@ public class RestPanel extends JPanel {
 		gbc_lblRest.gridy = 0;
 		gbc_lblRest.gridy = 0;
 		add(lblRest, gbc_lblRest);
 		add(lblRest, gbc_lblRest);
 		
 		
-		lblLocation = new JLabel(rest.getWhere().getHumanReadable());
+		lblLocation = new JLabel();
 		GridBagConstraints gbc_lblLocation = new GridBagConstraints();
 		GridBagConstraints gbc_lblLocation = new GridBagConstraints();
 		gbc_lblLocation.insets = new Insets(0, 0, 0, 5);
 		gbc_lblLocation.insets = new Insets(0, 0, 0, 5);
 		gbc_lblLocation.gridx = 1;
 		gbc_lblLocation.gridx = 1;
 		gbc_lblLocation.gridy = 0;
 		gbc_lblLocation.gridy = 0;
 		add(lblLocation, gbc_lblLocation);
 		add(lblLocation, gbc_lblLocation);
 		
 		
-		lblDuration = new JLabel(rest.getDuration().toString());
-		GridBagConstraints gbc_lblDuration = new GridBagConstraints();
-		gbc_lblDuration.gridx = 2;
-		gbc_lblDuration.gridy = 0;
-		add(lblDuration, gbc_lblDuration);
+		DurationPanel panelDuration = new DurationPanel("");
+		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
+		gbc_panelDuration.gridx = 2;
+		gbc_panelDuration.gridy = 0;
+		add(panelDuration, gbc_panelDuration);
+		
+		durationListener = ObservableController.from(panelDuration.txtTime,
+				Rest::getDuration, Rest::setDuration);
+	}
+
+	public RestPanel(final Rest rest) {
+		this();
+		setModel(rest);
+	}
+	
+	public void setModel(final Rest rest) {
+		lblLocation.setText(rest.getWhere().getHumanReadable());
+		durationListener.setObserved(rest);
 	}
 	}
 
 
 }
 }

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

@@ -4,6 +4,8 @@ import javax.swing.JPanel;
 import javax.swing.event.DocumentListener;
 import javax.swing.event.DocumentListener;
 
 
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.ObservableController;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
 import org.leumasjaffe.recipe.model.Step;
@@ -22,15 +24,19 @@ import javax.swing.JLabel;
 import javax.swing.JTextPane;
 import javax.swing.JTextPane;
 import java.awt.Component;
 import java.awt.Component;
 import javax.swing.Box;
 import javax.swing.Box;
+import javax.swing.JFormattedTextField;
+
 import java.awt.Dimension;
 import java.awt.Dimension;
 
 
 @SuppressWarnings("serial")
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+	ForwardingObservableListener<Step> listener = new ForwardingObservableListener<>();
+	ObservableListener<JFormattedTextField, Step> durationListener;
+
 	@Getter(AccessLevel.PACKAGE) JLabel lblIndex;
 	@Getter(AccessLevel.PACKAGE) JLabel lblIndex;
 	@Getter(AccessLevel.PACKAGE) JTextPane txtpnInstructions;
 	@Getter(AccessLevel.PACKAGE) JTextPane txtpnInstructions;
 	@Getter(AccessLevel.PACKAGE) AutoGrowPanel panelIngredients;
 	@Getter(AccessLevel.PACKAGE) AutoGrowPanel panelIngredients;
-	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();
@@ -103,8 +109,12 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 		gbc_txtpnInstructions.gridy = 0;
 		gbc_txtpnInstructions.gridy = 0;
 		add(txtpnInstructions, gbc_txtpnInstructions);
 		add(txtpnInstructions, gbc_txtpnInstructions);
 		
 		
+		durationListener = ObservableController.from(panelDuration.txtTime,
+				Step::getDuration, Step::setDuration);
+		
 		setListPosition(zeroIndex);
 		setListPosition(zeroIndex);
 		listener.setObserved(step, ingredients);
 		listener.setObserved(step, ingredients);
+		durationListener.setObserved(step);
 	}
 	}
 
 
 	@Override
 	@Override

+ 34 - 0
src/main/lombok/org/leumasjaffe/recipe/view/formatter/AmountFormatter.java

@@ -0,0 +1,34 @@
+package org.leumasjaffe.recipe.view.formatter;
+
+import java.text.ParseException;
+
+import javax.swing.text.DefaultFormatter;
+
+import org.leumasjaffe.recipe.model.Amount;
+
+public class AmountFormatter extends DefaultFormatter {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 7379502839670512936L;
+	
+	public AmountFormatter() {
+		setCommitsOnValidEdit(false);
+	}
+
+	@Override
+	public Amount stringToValue(String text) throws ParseException {
+		final int to = text.indexOf(' ');
+		try {
+			Float.parseFloat(to == -1 ? text : text.substring(0, to));
+		} catch (NumberFormatException nfe) {
+			throw new ParseException(nfe.getMessage(), 0);
+		}
+
+		// This is technically a little permissive, since we're not checking
+		// the validity of the unit type.
+		return new Amount(text);
+	}
+
+}

+ 49 - 0
src/main/lombok/org/leumasjaffe/recipe/view/formatter/DurationFormatter.java

@@ -0,0 +1,49 @@
+package org.leumasjaffe.recipe.view.formatter;
+
+import java.text.ParseException;
+
+import javax.swing.text.DefaultFormatter;
+
+import org.leumasjaffe.recipe.model.Duration;
+
+public class DurationFormatter extends DefaultFormatter {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -2233461250996247706L;
+	
+	public DurationFormatter() {
+		setCommitsOnValidEdit(false);
+	}
+
+	@Override
+	public Duration stringToValue(String text) throws ParseException {
+		int tokens = 0;
+		int from = 0;
+		for (int to = text.indexOf(' '); to != -1;
+				from = to + 1, to = text.indexOf(' ', from), ++tokens) {
+			// In order to pass muster - a string must match the following:
+			// Composed of either 2 or 4 tokens (space separated) matching the regex:
+			// (\d+(\.5)? - )?\d+(\.5)? (s|min|hr)
+			if (tokens == 1) { continue; } // Ignore '-'
+			else if (tokens == 3) {
+				throw new ParseException("Too many tokens in input", from);
+			}
+			try {
+				Float.parseFloat(text.substring(from, to));
+			} catch (NumberFormatException nfe) {
+				throw new ParseException(nfe.getMessage(), from);
+			}
+		}
+		
+		// Less efficient, but this allows me to punt the validation to the object
+		// once I'm confident it 'appears' correct.
+		try {
+			return new Duration(text);
+		} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ex) {
+			throw new ParseException("No valid duration interval was provided", from);
+		}
+	}
+
+}

+ 6 - 26
src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java

@@ -5,12 +5,9 @@ import java.awt.GridBagLayout;
 import javax.swing.JTextField;
 import javax.swing.JTextField;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Insets;
-import java.text.NumberFormat;
-import java.util.Locale;
-
-import javax.swing.text.NumberFormatter;
 
 
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Getter;
@@ -25,13 +22,12 @@ import javax.swing.JLabel;
 public class IngredientPanel extends JPanel {
 public class IngredientPanel extends JPanel {
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
-	@Getter(AccessLevel.PACKAGE) JTextField txtUnit;
 	
 	
 	public IngredientPanel() {
 	public IngredientPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
-		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		setLayout(gridBagLayout);
 		
 		
@@ -54,30 +50,15 @@ public class IngredientPanel extends JPanel {
 		add(txtName, gbc_txtName);
 		add(txtName, gbc_txtName);
 		txtName.setColumns(10);
 		txtName.setColumns(10);
 		
 		
-		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
-		fmtDone.setMinimum(0.0);
-		fmtDone.setCommitsOnValidEdit(true);
-		txtAmount = new JFormattedTextField(fmtDone);
+		txtAmount = new JFormattedTextField(new AmountFormatter());
 		txtAmount.setEditable(false);
 		txtAmount.setEditable(false);
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
-		gbc_txtAmount.insets = new Insets(0, 0, 0, 5);
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridx = 2;
 		gbc_txtAmount.gridy = 0;
 		gbc_txtAmount.gridy = 0;
 		add(txtAmount, gbc_txtAmount);
 		add(txtAmount, gbc_txtAmount);
-		txtAmount.setColumns(4);
-		
-		txtUnit = new JTextField();
-		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);
+		txtAmount.setColumns(11);
 	}
 	}
 
 
 	public IngredientPanel(final Ingredient ingredient) {
 	public IngredientPanel(final Ingredient ingredient) {
@@ -87,7 +68,6 @@ public class IngredientPanel extends JPanel {
 	
 	
 	public void setModel(final Ingredient ingredient) {
 	public void setModel(final Ingredient ingredient) {
 		txtName.setText(ingredient.getName());
 		txtName.setText(ingredient.getName());
-		txtAmount.setValue(ingredient.getAmount().getValue());
-		txtUnit.setText(ingredient.getAmount().unitName());
+		txtAmount.setValue(ingredient.getAmount());
 	}
 	}
 }
 }

+ 1 - 2
src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java

@@ -13,7 +13,6 @@ import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
-import org.leumasjaffe.recipe.model.CollatedDuration;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.CollatedDurationPanel;
 import org.leumasjaffe.recipe.view.CollatedDurationPanel;
@@ -72,7 +71,7 @@ public class SummaryPanel extends JPanel {
 		panelHeader.add(txtTitle, gbc_txtTitle);
 		panelHeader.add(txtTitle, gbc_txtTitle);
 		txtTitle.setColumns(10);
 		txtTitle.setColumns(10);
 		
 		
-		CollatedDurationPanel panelDuration = new CollatedDurationPanel(CollatedDuration.ZERO);
+		CollatedDurationPanel panelDuration = new CollatedDurationPanel();
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 0;
 		gbc_panelDuration.gridx = 0;
 		gbc_panelDuration.gridy = 1;
 		gbc_panelDuration.gridy = 1;

+ 28 - 0
src/test/java/org/leumasjaffe/recipe/model/DurationTest.java

@@ -96,5 +96,33 @@ class DurationTest {
 	void testHalfHourDisplayIsNotUsedForCloserToWhole(int value) {
 	void testHalfHourDisplayIsNotUsedForCloserToWhole(int value) {
 		assertNotEquals("0.5 hr", new Duration(HOURS, 0, value).toString());
 		assertNotEquals("0.5 hr", new Duration(HOURS, 0, value).toString());
 	}
 	}
+	
+	@Test
+	void testStringConstructorProducesCorrectData() {
+		assertEquals(new Duration(HOURS, 3600, 7200), new Duration("1 - 2 hr"));
+		assertEquals(new Duration(MINUTES, 600, 3600), new Duration("10 - 60 min"));
+		assertEquals(new Duration(SECONDS, 5, 10), new Duration("5 - 10 s"));
+
+		assertEquals(new Duration(HOURS, 0, 3600), new Duration("1 hr"));
+		assertEquals(new Duration(MINUTES, 0, 3600), new Duration("60 min"));
+		assertEquals(new Duration(SECONDS, 0, 10), new Duration("10 s"));
+	}
+	
+	@Test
+	void testStringConstructorThrowsOnUnknownDuration() {
+		assertThrows(IllegalArgumentException.class,
+				() -> new Duration("0.5 tocks"));
+	}
+	
+	@Test
+	void testStringConstructorProcessesFloats() {
+		assertEquals("0.5 hr", new Duration("0.5 hr").toString());
+	}
+	
+	@Test
+	void testStringConstructorCanBeLossy() {
+		assertEquals("0.5 hr", new Duration("0.25 hr").toString());
+		assertEquals("0 hr", new Duration("0.2 hr").toString());
+	}
 
 
 }
 }

+ 1 - 1
src/test/java/org/leumasjaffe/recipe/model/PhaseTest.java

@@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
 
 
 class PhaseTest {
 class PhaseTest {
 	private static final Amount _1g = new Amount("1 g");
 	private static final Amount _1g = new Amount("1 g");
-	private static final Duration dur = new Duration(Duration.Display.SECONDS, 10, 20);
+	private static final Duration dur = new Duration("10 - 20 s");
 
 
 	@Test
 	@Test
 	void cannotAddNullPreparation() {
 	void cannotAddNullPreparation() {

+ 7 - 24
src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java

@@ -35,8 +35,7 @@ class IngredientPanelTest extends SwingTestCase {
 	void testFilledOutWithContent() {
 	void testFilledOutWithContent() {
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Sliced", panel.getTxtPreparation().getText());
 		assertEquals("Sliced", panel.getTxtPreparation().getText());
-		assertEquals("100", panel.getTxtAmount().getText());
-		assertEquals("g", panel.getTxtUnit().getText());
+		assertEquals(new Amount("100 g"), panel.getTxtAmount().getValue());
 	}
 	}
 	
 	
 	@Test
 	@Test
@@ -44,7 +43,6 @@ class IngredientPanelTest extends SwingTestCase {
 		assertTrue(panel.getTxtName().isEditable());
 		assertTrue(panel.getTxtName().isEditable());
 		assertTrue(panel.getTxtPreparation().isEditable());
 		assertTrue(panel.getTxtPreparation().isEditable());
 		assertTrue(panel.getTxtAmount().isEditable());
 		assertTrue(panel.getTxtAmount().isEditable());
-		assertTrue(panel.getTxtUnit().isEditable());
 	}
 	}
 
 
 	@Test
 	@Test
@@ -57,8 +55,7 @@ class IngredientPanelTest extends SwingTestCase {
 		ObserverDispatch.notifySubscribers(stuff);
 		ObserverDispatch.notifySubscribers(stuff);
 		
 		
 		assertEquals("Bacon", panel.getTxtName().getText());
 		assertEquals("Bacon", panel.getTxtName().getText());
-		assertEquals(100.0, panel.getTxtAmount().getValue());
-		assertEquals("g", panel.getTxtUnit().getText());
+		assertEquals(new Amount("0.25 lb"), panel.getTxtAmount().getValue());
 		assertEquals("Cut into Lardons", panel.getTxtPreparation().getText());
 		assertEquals("Cut into Lardons", panel.getTxtPreparation().getText());
 	}
 	}
 
 
@@ -77,17 +74,10 @@ class IngredientPanelTest extends SwingTestCase {
 	}
 	}
 	
 	
 	@Test
 	@Test
-	void testViewUpdateToAmountDoesNotAltersModel() {
-		panel.getTxtAmount().setValue(0.25);
+	void testViewUpdateToAmountAltersModel() {
+		panel.getTxtAmount().setValue(new Amount("1 lb"));
 		waitForSwing();
 		waitForSwing();
-		assertEquals(100.0, stuff.getAmount().getValue());
-	}
-	
-	@Test
-	void testViewUpdateToUnitDoesNotAltersModel() {
-		panel.getTxtUnit().setText("lb");
-		waitForSwing();
-		assertEquals("g", stuff.getAmount().unitName());
+		assertEquals(new Amount("1 lb"), stuff.getAmount());
 	}
 	}
 
 
 	@Test
 	@Test
@@ -106,16 +96,9 @@ class IngredientPanelTest extends SwingTestCase {
 
 
 	@Test
 	@Test
 	void testUpdateToAmountSendsNotify() {
 	void testUpdateToAmountSendsNotify() {
-		panel.getTxtAmount().setValue(0.25);
+		panel.getTxtAmount().setValue(new Amount("1 lb"));
 		waitForSwing();
 		waitForSwing();
-		verify(listener, never()).updateWasSignalled();
+		verify(listener).updateWasSignalled();
 	}
 	}
 	
 	
-	@Test
-	void testUpdateToUnitSendsNotify() {
-		panel.getTxtUnit().setText("lb");
-		waitForSwing();
-		verify(listener, never()).updateWasSignalled();
-	}
-
 }
 }

+ 8 - 4
src/test/java/org/leumasjaffe/recipe/view/IngredientPreparationPanelTest.java

@@ -23,8 +23,7 @@ class IngredientPreparationPanelTest extends SwingTestCase {
 	void testFilledOutWithContent() {
 	void testFilledOutWithContent() {
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Sliced", panel.getTxtPreparation().getText());
 		assertEquals("Sliced", panel.getTxtPreparation().getText());
-		assertEquals("100", panel.getTxtAmount().getText());
-		assertEquals("g", panel.getTxtUnit().getText());
+		assertEquals(new Amount("100 g"), panel.getTxtAmount().getValue());
 	}
 	}
 	
 	
 	@Test
 	@Test
@@ -32,16 +31,21 @@ class IngredientPreparationPanelTest extends SwingTestCase {
 		assertFalse(panel.getTxtName().isEditable());
 		assertFalse(panel.getTxtName().isEditable());
 		assertFalse(panel.getTxtPreparation().isEditable());
 		assertFalse(panel.getTxtPreparation().isEditable());
 		assertFalse(panel.getTxtAmount().isEditable());
 		assertFalse(panel.getTxtAmount().isEditable());
-		assertFalse(panel.getTxtUnit().isEditable());
 	}
 	}
 
 
 	@Test
 	@Test
 	void testIsSubscribedToUpdates() {
 	void testIsSubscribedToUpdates() {
 		stuff.setName("Bacon");
 		stuff.setName("Bacon");
+		stuff.setPreparation("Cut into Lardons");
+		stuff.setAmount(new Amount("50 g"));
+		
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Onions", panel.getTxtName().getText());
+		
 		ObserverDispatch.notifySubscribers(stuff);
 		ObserverDispatch.notifySubscribers(stuff);
+		
 		assertEquals("Bacon", panel.getTxtName().getText());
 		assertEquals("Bacon", panel.getTxtName().getText());
-		// TODO: I need to add hook-ups for the rest of the fields, too
+		assertEquals("Cut into Lardons", panel.getTxtPreparation().getText());
+		assertEquals(new Amount("50 g"), panel.getTxtAmount().getValue());
 	}
 	}
 
 
 }
 }

+ 26 - 1
src/test/java/org/leumasjaffe/recipe/view/PhasePanelTest.java

@@ -3,6 +3,7 @@ package org.leumasjaffe.recipe.view;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
 import java.util.Arrays;
 import java.util.Arrays;
+import java.util.Optional;
 
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -11,7 +12,10 @@ import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Phase;
+import org.leumasjaffe.recipe.model.Preparation;
+import org.leumasjaffe.recipe.model.Rest;
 import org.leumasjaffe.recipe.model.Step;
 import org.leumasjaffe.recipe.model.Step;
 import org.mockito.Mock;
 import org.mockito.Mock;
 import org.mockito.Spy;
 import org.mockito.Spy;
@@ -22,13 +26,19 @@ import org.mockito.junit.jupiter.MockitoExtension;
 class PhasePanelTest extends SwingTestCase {
 class PhasePanelTest extends SwingTestCase {
 	
 	
 	@Spy MockObserverListener listener;
 	@Spy MockObserverListener listener;
+	
+	final Preparation prep = new Preparation();
 	final Step stub = new Step();
 	final Step stub = new Step();
+	final Rest rest = new Rest(Rest.Where.REFRIGERATOR, new Duration("10 s"));
+	
 	@Mock Phase stuff;
 	@Mock Phase stuff;
 	PhasePanel panel;
 	PhasePanel panel;
 
 
 	@BeforeEach
 	@BeforeEach
 	void setUp() {
 	void setUp() {
 		doReturn(Arrays.asList(stub)).when(stuff).getCooking();
 		doReturn(Arrays.asList(stub)).when(stuff).getCooking();
+		doReturn(Optional.of(prep)).when(stuff).getPreparation();
+		doReturn(Optional.of(rest)).when(stuff).getRest();
 		panel = new PhasePanel(stuff);
 		panel = new PhasePanel(stuff);
 		
 		
 		listener.setObserved(stuff);
 		listener.setObserved(stuff);
@@ -37,8 +47,23 @@ class PhasePanelTest extends SwingTestCase {
 	}
 	}
 
 
 	@Test
 	@Test
-	void testPropogatesSignalFromChildren() {
+	void testPropogatesSignalFromSteps() {
 		ObserverDispatch.notifySubscribers(stub);
 		ObserverDispatch.notifySubscribers(stub);
+		
+		verify(listener).updateWasSignalled();
+	}
+
+	@Test
+	void testPropogatesSignalFromPrep() {
+		ObserverDispatch.notifySubscribers(prep);
+		
+		verify(listener).updateWasSignalled();
+	}
+
+	@Test
+	void testPropogatesSignalFromRest() {
+		ObserverDispatch.notifySubscribers(rest);
+		
 		verify(listener).updateWasSignalled();
 		verify(listener).updateWasSignalled();
 	}
 	}
 
 

+ 7 - 20
src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java

@@ -2,12 +2,14 @@ package org.leumasjaffe.recipe.view;
 
 
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
+import javax.swing.JFormattedTextField;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
-import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.mockito.InjectMocks;
 import org.mockito.InjectMocks;
@@ -17,41 +19,26 @@ import org.mockito.junit.jupiter.MockitoExtension;
 @ExtendWith(MockitoExtension.class)
 @ExtendWith(MockitoExtension.class)
 class PreparationPanelTest extends SwingTestCase {
 class PreparationPanelTest extends SwingTestCase {
 	
 	
-	@Mock DurationPanel panelDuration;
+	@Mock ObservableListener<JFormattedTextField, Preparation> durationListener;
 	@Mock ReplaceChildrenController<Preparation, Ingredient> controller;
 	@Mock ReplaceChildrenController<Preparation, Ingredient> controller;
-	Preparation stuff;
+	Preparation stuff = new Preparation();
 	@InjectMocks PreparationPanel panel = new PreparationPanel();
 	@InjectMocks PreparationPanel panel = new PreparationPanel();
 	
 	
 	@BeforeEach
 	@BeforeEach
 	void setUp() {
 	void setUp() {
-		stuff = mock(Preparation.class);
-		Duration dur = new Duration(Duration.Display.SECONDS, 0, 30);
-		doReturn(dur).when(stuff).getDuration();
-		
 		panel.setModel(stuff);
 		panel.setModel(stuff);
 	}
 	}
 
 
 	@Test
 	@Test
 	void testHasContent() {
 	void testHasContent() {
-		verify(panelDuration).setModel(any());
+		verify(durationListener, times(1)).setObserved(same(stuff));
 		verify(controller, times(1)).accept(any(), same(stuff));
 		verify(controller, times(1)).accept(any(), same(stuff));
 	}
 	}
 
 
-	@Test
-	void testDoesNotUpdateDurationWhenNotified() {
-		clearInvocations(panelDuration);
-
-		ObserverDispatch.notifySubscribers(stuff);		
-
-		verify(panelDuration, never()).setModel(any());
-	}
-
 	@Test
 	@Test
 	void testUpdatesNumberOfChildrenWhenNotified() {
 	void testUpdatesNumberOfChildrenWhenNotified() {
-		clearInvocations((Object) controller);
-		
 		ObserverDispatch.notifySubscribers(stuff);		
 		ObserverDispatch.notifySubscribers(stuff);		
 
 
-		verify(controller, times(1)).accept(any(), same(stuff));
+		verify(controller, times(2)).accept(any(), same(stuff));
 	}
 	}
 }
 }

+ 25 - 13
src/test/java/org/leumasjaffe/recipe/view/RestPanelTest.java

@@ -1,40 +1,52 @@
 package org.leumasjaffe.recipe.view;
 package org.leumasjaffe.recipe.view;
 
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.StringContains.containsString;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import javax.swing.JFormattedTextField;
+import javax.swing.JLabel;
 
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
+import org.leumasjaffe.mock.MockObserverListener;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Rest;
 import org.leumasjaffe.recipe.model.Rest;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 
 @ExtendWith(MockitoExtension.class)
 @ExtendWith(MockitoExtension.class)
 @RunWith(JUnitPlatform.class)
 @RunWith(JUnitPlatform.class)
 class RestPanelTest extends SwingTestCase {
 class RestPanelTest extends SwingTestCase {
 	
 	
-	Duration dur;
+	@Spy MockObserverListener listener;
+	
 	Rest stuff;
 	Rest stuff;
-	RestPanel panel;
+	
+	@Spy JLabel lblLocation;
+	@Mock ObservableListener<JFormattedTextField, Rest> durationListener;
+	@InjectMocks RestPanel panel = new RestPanel();
 	
 	
 	@BeforeEach
 	@BeforeEach
 	void setUp() {
 	void setUp() {
-		dur = new Duration(Duration.Display.SECONDS, 0, 30);
-		stuff = new Rest(Rest.Where.REFRIGERATOR, dur);
-		panel = new RestPanel(stuff);
+		stuff = new Rest(Rest.Where.REFRIGERATOR, new Duration(Duration.Display.SECONDS, 0, 30));
+		panel.setModel(stuff);
+		
+		listener.setObserved(stuff);
+		clearInvocations(listener);
 	}
 	}
 
 
 	@Test
 	@Test
 	void testHasContent() {
 	void testHasContent() {
-		assertThat(panel.getLblDuration().getText(),
-				containsString(dur.toString()));
-		assertEquals(stuff.getWhere().getHumanReadable(),
-				panel.getLblLocation().getText());
+		verify(lblLocation).setText(eq(stuff.getWhere().getHumanReadable()));
 	}
 	}
 
 
-	// TODO: Hook-ups for editing the preparation time
+	@Test
+	void testDurationIsListeningToModel() {
+		verify(durationListener).setObserved(same(stuff));
+	}
 }
 }

+ 40 - 0
src/test/java/org/leumasjaffe/recipe/view/formatter/AmountFormatterTest.java

@@ -0,0 +1,40 @@
+package org.leumasjaffe.recipe.view.formatter;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.text.ParseException;
+
+import org.junit.jupiter.api.Test;
+
+class AmountFormatterTest {
+	
+	AmountFormatter formatter = new AmountFormatter();
+
+	@Test
+	void testCanParseValidStrings() {
+		assertDoesNotThrow(() -> formatter.stringToValue("1"));
+		assertDoesNotThrow(() -> formatter.stringToValue("1 ct"));
+		assertDoesNotThrow(() -> formatter.stringToValue("1 g"));
+		assertDoesNotThrow(() -> formatter.stringToValue("1 tsp"));
+	}
+	
+	@Test
+	void testCanParseRandomNames() {
+		assertDoesNotThrow(() -> formatter.stringToValue("1 large"));
+		assertDoesNotThrow(() -> formatter.stringToValue("1 bacon pancake"));
+	}
+
+
+	@Test
+	void testThrowsErrorOnInvalidFloat() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0.Q"));
+	}
+
+	@Test
+	void testThrowsErrorEmptyString() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue(""));
+	}
+
+}

+ 56 - 0
src/test/java/org/leumasjaffe/recipe/view/formatter/DurationFormatterTest.java

@@ -0,0 +1,56 @@
+package org.leumasjaffe.recipe.view.formatter;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.text.ParseException;
+
+import org.junit.jupiter.api.Test;
+
+class DurationFormatterTest {
+	
+	DurationFormatter formatter = new DurationFormatter();
+
+	@Test
+	void testCanParseValidStrings() {
+		assertDoesNotThrow(() -> formatter.stringToValue("10 s"));
+		assertDoesNotThrow(() -> formatter.stringToValue("10 - 20 s"));
+	}
+	
+	@Test
+	void testThrowsErrorOnUnknownEnum() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("10 tocks"));
+	}
+
+	@Test
+	void testThrowsErrorOnInvalidFloat() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0.Q s"));
+	}
+
+	@Test
+	void testThrowsErrorEmptyString() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue(""));
+	}
+	
+	@Test
+	void testThrowsErrorOneArg() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0"));
+	}
+
+	@Test
+	void testThrowsErrorThreeArgs() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0 - s"));
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0 1 s"));
+	}
+
+	@Test
+	void testThrowsErrorMoreThanFourArgs() {
+		assertThrows(ParseException.class,
+				() -> formatter.stringToValue("0 - 1 s s"));
+	}
+}

+ 1 - 3
src/test/java/org/leumasjaffe/recipe/view/summary/IngredientPanelTest.java

@@ -23,15 +23,13 @@ class IngredientPanelTest extends SwingTestCase {
 	@Test
 	@Test
 	void testFilledOutWithContent() {
 	void testFilledOutWithContent() {
 		assertEquals("Onions", panel.getTxtName().getText());
 		assertEquals("Onions", panel.getTxtName().getText());
-		assertEquals("100", panel.getTxtAmount().getText());
-		assertEquals("g", panel.getTxtUnit().getText());
+		assertEquals(new Amount("100 g"), panel.getTxtAmount().getValue());
 	}
 	}
 	
 	
 	@Test
 	@Test
 	void testCannotEditContent() {
 	void testCannotEditContent() {
 		assertFalse(panel.getTxtName().isEditable());
 		assertFalse(panel.getTxtName().isEditable());
 		assertFalse(panel.getTxtAmount().isEditable());
 		assertFalse(panel.getTxtAmount().isEditable());
-		assertFalse(panel.getTxtUnit().isEditable());
 	}
 	}
 
 
 	@Test
 	@Test

+ 6 - 30
src/test/resources/example.json

@@ -10,11 +10,7 @@
           "dependsOn": [],
           "dependsOn": [],
           "vessel": "",
           "vessel": "",
           "preparation": {
           "preparation": {
-            "duration": {
-              "displayAs": "MINUTES",
-              "minSeconds": 300,
-              "maxSeconds": 600
-            }
+            "duration": "5 - 10 min"
           },
           },
           "cooking": [
           "cooking": [
             {
             {
@@ -25,11 +21,7 @@
                   "amount": "1 Tbsp"
                   "amount": "1 Tbsp"
                 }
                 }
               ],
               ],
-              "duration": {
-                "displayAs": "SECONDS",
-                "minSeconds": 30,
-                "maxSeconds": 60
-              },
+              "duration": "30 - 60 s",
               "instruction": "Heat oil over high heat"
               "instruction": "Heat oil over high heat"
             },
             },
             {
             {
@@ -40,21 +32,13 @@
                   "amount": "100 g"
                   "amount": "100 g"
                 }
                 }
               ],
               ],
-              "duration": {
-                "displayAs": "MINUTES",
-                "minSeconds": 300,
-                "maxSeconds": 900
-              },
+              "duration": "5 - 10 min",
               "instruction": "Sauté the onions until soft and translucent, stirring to prevent burning"
               "instruction": "Sauté the onions until soft and translucent, stirring to prevent burning"
             }
             }
           ],
           ],
           "rest": {
           "rest": {
             "where": "ROOM_TEMPERATURE",
             "where": "ROOM_TEMPERATURE",
-            "duration": {
-              "displayAs": "SECONDS",
-              "minSeconds": 0,
-              "maxSeconds": 30
-            }
+            "duration": "30 s"
           }
           }
         },
         },
         {
         {
@@ -70,11 +54,7 @@
                   "amount": "1 Tbsp"
                   "amount": "1 Tbsp"
                 }
                 }
               ],
               ],
-              "duration": {
-                "displayAs": "SECONDS",
-                "minSeconds": 30,
-                "maxSeconds": 60
-              },
+              "duration": "30 - 60 s",
               "instruction": "Heat oil over high heat"
               "instruction": "Heat oil over high heat"
             },
             },
             {
             {
@@ -85,11 +65,7 @@
                   "amount": "100 g"
                   "amount": "100 g"
                 }
                 }
               ],
               ],
-              "duration": {
-                "displayAs": "MINUTES",
-                "minSeconds": 300,
-                "maxSeconds": 900
-              },
+              "duration": "5 - 10 min",
               "instruction": "Sauté the onions until soft and translucent, stirring to prevent burning"
               "instruction": "Sauté the onions until soft and translucent, stirring to prevent burning"
             }
             }
           ]
           ]