Browse Source

Merge branch 'feat/viewmodel/scale'

* feat/viewmodel/scale:
  Add a ScaleFactor object.
  Switch from the actual servings spinner to the recipe scale spinner when going read only.
  Wrap view menu in object. Add accelerator. Move menu objects into package.
  Add the ability to enable/disable editing for recipes.
  Don't duplicate ingredientpanel code for a read-only version.
Sam Jaffe 4 years ago
parent
commit
ed1349107c
32 changed files with 265 additions and 209 deletions
  1. 3 1
      src/main/lombok/org/leumasjaffe/recipe/controller/AddPhaseAction.java
  2. 4 0
      src/main/lombok/org/leumasjaffe/recipe/model/Amount.java
  3. 7 0
      src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java
  4. 26 8
      src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java
  5. 23 7
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java
  6. 0 98
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java
  7. 17 4
      src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java
  8. 14 5
      src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java
  9. 12 2
      src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java
  10. 5 0
      src/main/lombok/org/leumasjaffe/recipe/view/RecipeManagerFrame.java
  11. 19 3
      src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java
  12. 15 5
      src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java
  13. 1 1
      src/main/lombok/org/leumasjaffe/recipe/view/FileMenu.java
  14. 28 0
      src/main/lombok/org/leumasjaffe/recipe/view/menu/ViewMenu.java
  15. 3 2
      src/main/lombok/org/leumasjaffe/recipe/view/summary/ElementPanel.java
  16. 13 4
      src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java
  17. 28 5
      src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java
  18. 20 0
      src/main/lombok/org/leumasjaffe/recipe/viewmodel/ScaleFactor.java
  19. 2 0
      src/test/java/org/leumasjaffe/recipe/controller/AddPhaseActionTest.java
  20. 2 1
      src/test/java/org/leumasjaffe/recipe/view/ElementPanelTest.java
  21. 2 1
      src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java
  22. 0 51
      src/test/java/org/leumasjaffe/recipe/view/IngredientPreparationPanelTest.java
  23. 2 1
      src/test/java/org/leumasjaffe/recipe/view/PhasePanelIT.java
  24. 2 1
      src/test/java/org/leumasjaffe/recipe/view/PhasePanelTest.java
  25. 2 1
      src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java
  26. 2 1
      src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java
  27. 2 1
      src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java
  28. 3 2
      src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java
  29. 2 1
      src/test/java/org/leumasjaffe/recipe/view/FileMenuTest.java
  30. 2 1
      src/test/java/org/leumasjaffe/recipe/view/summary/ElementPanelTest.java
  31. 2 1
      src/test/java/org/leumasjaffe/recipe/view/summary/IngredientPanelTest.java
  32. 2 1
      src/test/java/org/leumasjaffe/recipe/view/summary/SummaryPanelTest.java

+ 3 - 1
src/main/lombok/org/leumasjaffe/recipe/controller/AddPhaseAction.java

@@ -9,6 +9,7 @@ import javax.swing.JSeparator;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.view.PhasePanel;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
@@ -20,13 +21,14 @@ import lombok.experimental.NonFinal;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class AddPhaseAction implements ActionListener {
 	JPanel view;
+	ScaleFactor scale;
 	@NonFinal @Setter Element model = null;
 
 	@Override
 	public void actionPerformed(ActionEvent e) {
 		Phase newPhase = new Phase();
 		this.model.getPhases().add(newPhase);
-		view.add(new PhasePanel(newPhase));
+		view.add(new PhasePanel(newPhase, scale));
 		view.add(new JSeparator());
 		view.revalidate();
 	}

+ 4 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Amount.java

@@ -96,6 +96,10 @@ public class Amount {
 		}
 		return new Amount(unit, value + amount.value * scale(amount), vol, wgt);
 	}
+	
+	public Amount scale(final double factor) {
+		return new Amount(unit, value * factor, vol, wgt);
+	}
 
 	private double scale(final Amount amount) {
 		switch (unit) {

+ 7 - 0
src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java

@@ -158,6 +158,13 @@ public class AutoGrowPanel<C extends Component & AutoGrowPanel.ChildComponent, T
 		models.remove(index);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		members.forEach(c -> c.setEnabled(enabled));
+		last().setVisible(enabled);
+	}
+	
 	private int lastIndex() { return members.size() - 1; }
 	
 	private C last() { return members.get(lastIndex()); }

+ 26 - 8
src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java

@@ -8,6 +8,7 @@ import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.leumasjaffe.recipe.controller.AddPhaseAction;
 import org.leumasjaffe.recipe.model.Element;
 
@@ -26,6 +27,7 @@ import java.awt.GridBagLayout;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.event.FocusListener;
+import java.util.stream.Stream;
 import java.awt.Component;
 import java.awt.Dimension;
 
@@ -40,12 +42,13 @@ public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComp
 	ObservableListener<CollatedDurationPanel, Element> durationListener;
 
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
+	JButton btnAdd;
 	AddPhaseAction addPhase;
 	JPanel panelViewPort;
+	ScaleFactor scale;
 	
-	public ElementPanel() {
-		setPreferredSize(new Dimension(600, 450));
-
+	public ElementPanel(final ScaleFactor scale) {
+		this.scale = scale;
 		setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
 		setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
 
@@ -66,7 +69,7 @@ public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComp
 		gbc_txtName.gridy = 0;
 		panelColumnHeader.add(txtName, gbc_txtName);
 		
-		JButton btnAdd = new JButton("+");
+		btnAdd = new JButton("+");
 		btnAdd.setToolTipText("Add new phase");
 		btnAdd.setPreferredSize(new Dimension(17, 17));
 		btnAdd.setMargin(new Insets(0, 0, 0, 0));
@@ -93,17 +96,19 @@ public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComp
 		setViewportView(panelViewPort);
 		panelViewPort.setLayout(new VerticalLayout(5));
 		
-		addPhase = new AddPhaseAction(panelViewPort);
+		addPhase = new AddPhaseAction(panelViewPort, scale);
 		btnAdd.addActionListener(addPhase);
 
 		nameController = ObservableController.from(txtName,
 				Element::getName, Element::setName);
 		durationListener = new ObservableListener<>(panelDuration,
 				(c, v) -> c.setModel(v.getCollatedDuration()));
+		
+		setEnabled(true);
 	}
 	
-	public ElementPanel(final Element element) {
-		this();
+	public ElementPanel(final Element element, final ScaleFactor scale) {
+		this(scale);
 		setModel(element);
 	}
 	
@@ -112,7 +117,7 @@ public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComp
 		
 		panelViewPort.removeAll();
 		for (final Phase phase : element.getPhases()) {
-			panelViewPort.add(new PhasePanel(phase));
+			panelViewPort.add(new PhasePanel(phase, scale));
 			panelViewPort.add(new JSeparator());
 		}
 		
@@ -121,6 +126,19 @@ public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComp
 		durationListener.setObserved(element);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		if (enabled) {
+			setPreferredSize(new Dimension(600, 450));
+		} else {
+			setPreferredSize(null);
+		}
+		txtName.setEditable(enabled);
+		btnAdd.setEnabled(enabled);
+		Stream.of(panelViewPort.getComponents()).forEach(c -> c.setEnabled(enabled));
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

+ 23 - 7
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -9,11 +9,13 @@ import java.awt.event.FocusListener;
 
 import javax.swing.event.DocumentListener;
 
+import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -28,11 +30,15 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 	ObservableListener<JTextField, Ingredient> nameController;
 	ObservableListener<JFormattedTextField, Ingredient> amountController;
 	ObservableListener<JTextField, Ingredient> preparationController;
+	ForwardingObservableListener<Ingredient> forward = new ForwardingObservableListener<>();
+	
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
+	ScaleFactor scale;
 		
-	public IngredientPanel() {
+	public IngredientPanel(final ScaleFactor scale, boolean editable) {
+		this.scale = scale;
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -49,6 +55,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 		add(label, gbc_label);
 		
 		txtName = new JTextField();
+		txtName.setEditable(editable);
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
 		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtName.insets = new Insets(0, 0, 0, 5);
@@ -58,6 +65,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 		txtName.setColumns(15);
 		
 		txtAmount = new JFormattedTextField(new AmountFormatter());
+		txtAmount.setEditable(editable);
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
 		gbc_txtAmount.insets = new Insets(0, 0, 0, 5);
@@ -67,6 +75,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 		txtAmount.setColumns(6);
 		
 		txtPreparation = new JTextField();
+		txtPreparation.setEditable(editable);
 		GridBagConstraints gbc_txtPreparation = new GridBagConstraints();
 		gbc_txtPreparation.anchor = GridBagConstraints.ABOVE_BASELINE;
 		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
@@ -75,22 +84,20 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 		add(txtPreparation, gbc_txtPreparation);
 		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.
 		nameController = ObservableController.from(txtName,
 				Ingredient::getName, Ingredient::setName);
 		amountController = ObservableController.from(txtAmount,
-				Ingredient::getAmount, Ingredient::setAmount);
+				i -> i.getAmount().scale(scale.getScale()), Ingredient::setAmount);
 		preparationController = ObservableController.from(txtPreparation,
 				Ingredient::getPreparation, Ingredient::setPreparation,
 				ing -> {
 					ing.setPreparation("");
 					ObserverDispatch.notifySubscribers(ing);
-				});
+				});		
 	}
 
-	public IngredientPanel(final Ingredient ingredient) {
-		this();
+	public IngredientPanel(final Ingredient ingredient, final ScaleFactor scale, boolean editable) {
+		this(scale, editable);
 		setModel(ingredient);
 	}
 	
@@ -98,6 +105,15 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildCompon
 		nameController.setObserved(ingredient);
 		amountController.setObserved(ingredient);
 		preparationController.setObserved(ingredient);
+		forward.setObserved(ingredient, scale);
+	}
+	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		txtName.setEditable(enabled);
+		txtAmount.setEditable(enabled);
+		txtPreparation.setEditable(enabled);
 	}
 	
 	@Override

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

@@ -1,98 +0,0 @@
-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 org.leumasjaffe.observer.ObservableListener;
-import org.leumasjaffe.observer.ObserverDispatch;
-import org.leumasjaffe.recipe.model.Ingredient;
-import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
-
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.experimental.FieldDefaults;
-
-import javax.swing.JFormattedTextField;
-import javax.swing.JLabel;
-
-@SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class IngredientPreparationPanel extends JPanel {
-	
-	ObservableListener<IngredientPreparationPanel, Ingredient> listener;
-	@Getter(AccessLevel.PACKAGE) JTextField txtName;
-	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
-	@Getter(AccessLevel.PACKAGE) JTextField txtPreparation;
-	
-	public IngredientPreparationPanel() {
-		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);
-		
-		txtName = new JTextField();
-		txtName.setEditable(false);
-		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(15);
-		
-		txtAmount = new JFormattedTextField(new AmountFormatter());
-		txtAmount.setEditable(false);
-		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(6);
-		
-		txtPreparation = new JTextField();
-		txtPreparation.setEditable(false);
-		GridBagConstraints gbc_txtPreparation = new GridBagConstraints();
-		gbc_txtPreparation.anchor = GridBagConstraints.ABOVE_BASELINE;
-		gbc_txtPreparation.fill = GridBagConstraints.HORIZONTAL;
-		gbc_txtPreparation.gridx = 3;
-		gbc_txtPreparation.gridy = 0;
-		add(txtPreparation, gbc_txtPreparation);
-		txtPreparation.setColumns(10);
-		
-		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);
-	}
-	
-	@Override
-	public void removeNotify() {
-		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(listener);
-	}
-
-}

+ 17 - 4
src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java

@@ -10,6 +10,7 @@ import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Step;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -22,19 +23,23 @@ import org.jdesktop.swingx.VerticalLayout;
 public class PhasePanel extends JPanel {
 	ForwardingObservableListener<Phase> listener = new ForwardingObservableListener<>();
 
+	PreparationPanel panelPrep;
+	RestPanel panelRest;
 	@Getter(AccessLevel.PACKAGE) AutoGrowPanel<StepPanel, Step> panelSteps;
 	
 	// TODO Re-configure to support this(); setModel(phase); pattern.
-	public PhasePanel(final Phase phase) {		
+	public PhasePanel(final Phase phase, final ScaleFactor scale) {		
 		setLayout(new VerticalLayout(5));
 		
-		add(new PreparationPanel(phase));
+		panelPrep = new PreparationPanel(phase, scale);
+		add(panelPrep);
 		
-		panelSteps = new AutoGrowPanel<>(Step::new, StepPanel::new);
+		panelSteps = new AutoGrowPanel<>(Step::new, s -> new StepPanel(s, scale));
 		panelSteps.setGap(5);
 		add(panelSteps);
 		
-		add(new RestPanel(phase.getRest()));
+		panelRest = new RestPanel(phase.getRest());
+		add(panelRest);
 
 		panelSteps.setModel(phase.getCooking(), added -> {
 			listener.setObserved(phase, allChildren(phase));
@@ -52,6 +57,14 @@ public class PhasePanel extends JPanel {
 		return children;
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		panelPrep.setEnabled(enabled);
+		panelSteps.setEnabled(enabled);
+		panelRest.setEnabled(enabled);
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

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

@@ -12,6 +12,7 @@ import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Preparation;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -33,9 +34,11 @@ public class PreparationPanel extends JPanel {
 	IndirectObservableListener<JPanel, Preparation> childListener;
 	ObservableListener<JFormattedTextField, Preparation> durationController;
 	
-	public PreparationPanel() {
+	DurationPanel panelDuration;
+	
+	public PreparationPanel(final ScaleFactor scale) {
 		controller = new ReplaceChildrenAction<>(Preparation::getIngredients,
-				IngredientPreparationPanel::new);
+				ing -> new IngredientPanel(ing, scale, false));
 		
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
@@ -73,7 +76,7 @@ public class PreparationPanel extends JPanel {
 		gbc_horizontalGlue.gridy = 0;
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		
-		DurationPanel panelDuration = new DurationPanel("Requires", Duration.ZERO);
+		panelDuration = new DurationPanel("Requires", Duration.ZERO);
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridy = 0;
@@ -96,8 +99,8 @@ public class PreparationPanel extends JPanel {
 				Preparation::getDuration, Preparation::setDuration);
 	}
 	
-	public PreparationPanel(final Phase phase) {
-		this();
+	public PreparationPanel(final Phase phase, final ScaleFactor scale) {
+		this(scale);
 		setModel(phase);
 	}
 	
@@ -107,6 +110,12 @@ public class PreparationPanel extends JPanel {
 		durationController.setObserved(prep);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		panelDuration.txtTime.setEditable(enabled);
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

+ 12 - 2
src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java

@@ -7,6 +7,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.summary.SummaryPanel;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -21,14 +22,15 @@ import javax.swing.ScrollPaneConstants;
 public class RecipeCardPanel extends JSplitPane {
 	ForwardingObservableListener<RecipeCard> listener;
 	
+	ScaleFactor scale = new ScaleFactor();
 	SummaryPanel summaryPanel;
 	AutoGrowPanel<ElementPanel, Element> panelElements;
 	
 	public RecipeCardPanel() {
 		setPreferredSize(new Dimension(1100, 600));
 
-		summaryPanel = new SummaryPanel();
-		panelElements = new AutoGrowPanel<>(Element::new, ElementPanel::new);
+		summaryPanel = new SummaryPanel(scale);
+		panelElements = new AutoGrowPanel<>(Element::new, e -> new ElementPanel(e, scale));
 		panelElements.setGap(5);
 
 		final JScrollPane scrollPane = new JScrollPane(panelElements);
@@ -46,6 +48,7 @@ public class RecipeCardPanel extends JSplitPane {
 	}
 	
 	public void setModel(final RecipeCard card) {
+		scale.setModel(card);
 		summaryPanel.setModel(card);
 		panelElements.setModel(card.getElements(), added -> {
 			listener.setObserved(card, card.getElements());
@@ -62,5 +65,12 @@ public class RecipeCardPanel extends JSplitPane {
 		super.removeNotify();
 		ObserverDispatch.unsubscribeAll(listener);
 	}
+	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		summaryPanel.setEnabled(enabled);
+		panelElements.setEnabled(enabled);
+	}
 
 }

+ 5 - 0
src/main/lombok/org/leumasjaffe/recipe/view/RecipeManagerFrame.java

@@ -4,6 +4,8 @@ import javax.swing.JFrame;
 
 import org.leumasjaffe.recipe.controller.FileController;
 import org.leumasjaffe.recipe.model.RecipeCard;
+import org.leumasjaffe.recipe.view.menu.FileMenu;
+import org.leumasjaffe.recipe.view.menu.ViewMenu;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -26,6 +28,9 @@ public class RecipeManagerFrame extends JFrame implements FileController.ViewMod
 		JMenu mnFile = new FileMenu(this, fileController);
 		menuBar.add(mnFile);
 		
+		JMenu mnView = new ViewMenu(this);
+		menuBar.add(mnView);
+				
 		fileController.create();
 
 		pack();

+ 19 - 3
src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java

@@ -25,6 +25,8 @@ public class RestPanel extends JPanel {
 	ObservableListener<JFormattedTextField, Rest> upToController;
 
 	JComboBox<Rest.Where> jcbLocation;
+	DurationPanel panelDuration;
+	DurationPanel panelUpTo;
 	
 	// TODO
 	Rest model;
@@ -51,20 +53,21 @@ public class RestPanel extends JPanel {
 		gbc_lblLocation.gridy = 0;
 		add(jcbLocation, gbc_lblLocation);
 		
-		DurationPanel panelDuration = new DurationPanel("");
+		panelDuration = new DurationPanel("");
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridy = 0;
 		add(panelDuration, gbc_panelDuration);
 
-		DurationPanel panelUpTo = new DurationPanel("and up to");
+		panelUpTo = new DurationPanel("and up to");
 		GridBagConstraints gbc_panelUpTo = new GridBagConstraints();
 		gbc_panelUpTo.gridx = 3;
 		gbc_panelUpTo.gridy = 0;
 		add(panelUpTo, gbc_panelUpTo);
 
 		jcbLocation.addItemListener(e -> {
-			panelDuration.txtTime.setEditable(!e.getItem().equals(Rest.Where.NONE));
+			panelDuration.txtTime.setEditable(!isResting());
+			panelUpTo.txtTime.setEditable(!isResting());
 			this.model.setWhere(Rest.Where.class.cast(e.getItem()));
 		});
 		durationController = ObservableController.from(panelDuration.txtTime,
@@ -73,6 +76,7 @@ public class RestPanel extends JPanel {
 				Rest::getUpTo, Rest::setUpTo);
 		
 		panelDuration.txtTime.setEditable(false);
+		panelUpTo.txtTime.setEditable(false);
 	}
 
 	public RestPanel(final Rest rest) {
@@ -87,10 +91,22 @@ public class RestPanel extends JPanel {
 		upToController.setObserved(rest);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		jcbLocation.setEnabled(enabled);
+		panelDuration.txtTime.setEditable(enabled && !isResting());
+		panelUpTo.txtTime.setEditable(enabled && !isResting());
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();
 		ObserverDispatch.unsubscribeAll(durationController);
 	}
 
+	private boolean isResting() {
+		return jcbLocation.getSelectedItem().equals(Rest.Where.NONE);
+	}
+
 }

+ 15 - 5
src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java

@@ -10,6 +10,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.TextBinding;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -39,8 +40,9 @@ public class StepPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 	JLabel lblIndex;
 	@Getter(AccessLevel.PACKAGE) JTextPane txtpnInstructions;
 	AutoGrowPanel<IngredientPanel, Ingredient> panelIngredients;
+	DurationPanel panelDuration;
 		
-	public StepPanel() {
+	public StepPanel(final ScaleFactor scale) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -77,13 +79,13 @@ public class StepPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 		gbc_horizontalGlue.gridy = 0;
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		
-		DurationPanel panelDuration = new DurationPanel("Requires");
+		panelDuration = new DurationPanel("Requires");
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridy = 0;
 		panelLeft.add(panelDuration, gbc_panelDuration);
 		
-		panelIngredients = new AutoGrowPanel<>(Ingredient::new, IngredientPanel::new);
+		panelIngredients = new AutoGrowPanel<>(Ingredient::new, ing -> new IngredientPanel(ing, scale, true));
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
 		gbc_panelIngredients.gridwidth = 3;
 		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
@@ -108,8 +110,8 @@ public class StepPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 		setListPosition(0);
 	}
 	
-	public StepPanel(final Step step) {
-		this();
+	public StepPanel(final Step step, final ScaleFactor scale) {
+		this(scale);
 		setModel(step);
 	}
 	
@@ -125,6 +127,14 @@ public class StepPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 		durationController.setObserved(step);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		panelDuration.txtTime.setEditable(enabled);
+		txtpnInstructions.setEditable(enabled);
+		panelIngredients.setEnabled(enabled);
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

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

@@ -1,4 +1,4 @@
-package org.leumasjaffe.recipe.view;
+package org.leumasjaffe.recipe.view.menu;
 
 import java.awt.Toolkit;
 import java.awt.event.InputEvent;

+ 28 - 0
src/main/lombok/org/leumasjaffe/recipe/view/menu/ViewMenu.java

@@ -0,0 +1,28 @@
+package org.leumasjaffe.recipe.view.menu;
+
+import java.awt.Toolkit;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.KeyStroke;
+
+import org.leumasjaffe.recipe.view.RecipeCardPanel;
+
+@SuppressWarnings("serial")
+public class ViewMenu extends JMenu {
+	static final int MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
+
+	public ViewMenu(JFrame parent) {
+		super("View");
+		
+		JCheckBoxMenuItem chckbxmntmReadOnly = new JCheckBoxMenuItem("Read Only");
+		add(chckbxmntmReadOnly);
+		chckbxmntmReadOnly.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, MASK));
+		chckbxmntmReadOnly.addActionListener(e -> {
+			((RecipeCardPanel) parent.getContentPane()).setEnabled(!chckbxmntmReadOnly.getState());
+		});
+	}
+
+}

+ 3 - 2
src/main/lombok/org/leumasjaffe/recipe/view/summary/ElementPanel.java

@@ -8,6 +8,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenAction;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -29,9 +30,9 @@ public class ElementPanel extends JPanel {
 	@Getter(AccessLevel.PACKAGE) JLabel lblProductName;
 	@Getter(AccessLevel.PACKAGE) JPanel panelIngredients;
 	
-	public ElementPanel(final Element element) {
+	public ElementPanel(final Element element, final ScaleFactor scale) {
 		controller = new ReplaceChildrenAction<>(
-				Element::getIngredients, IngredientPanel::new);
+				Element::getIngredients, i -> new IngredientPanel(i, scale));
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0, 0};

+ 13 - 4
src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java

@@ -6,8 +6,10 @@ import javax.swing.JTextField;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 
+import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.view.formatter.AmountFormatter;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -19,10 +21,13 @@ import javax.swing.JLabel;
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class IngredientPanel extends JPanel {
+	ScaleFactor scale;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
+	IndirectObservableListener<JFormattedTextField, Ingredient> listener;
 	
-	public IngredientPanel() {
+	public IngredientPanel(final ScaleFactor scale) {
+		this.scale = scale;
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -56,15 +61,19 @@ public class IngredientPanel extends JPanel {
 		gbc_txtAmount.gridy = 0;
 		add(txtAmount, gbc_txtAmount);
 		txtAmount.setColumns(6);
+		
+		listener = new IndirectObservableListener<>(txtAmount,
+				(c, i) -> c.setValue(i.getAmount().scale(scale.getScale())));
 	}
 
-	public IngredientPanel(final Ingredient ingredient) {
-		this();
+	public IngredientPanel(final Ingredient ingredient, final ScaleFactor scale) {
+		this(scale);
 		setModel(ingredient);
 	}
 	
 	public void setModel(final Ingredient ingredient) {
 		txtName.setText(ingredient.getName());
-		txtAmount.setValue(ingredient.getAmount());
+		listener.setObserved(ingredient, ingredient, scale);
+		txtAmount.setValue(ingredient.getAmount().scale(scale.getScale()));
 	}
 }

+ 28 - 5
src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java

@@ -20,6 +20,7 @@ import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.CollatedDurationPanel;
 import org.leumasjaffe.recipe.view.ImagePanel;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -32,17 +33,20 @@ public class SummaryPanel extends JPanel {
 	ReplaceChildrenAction<RecipeCard, Element> controller;
 	TextBinding<RecipeCard> titleBinding;
 	SpinnerBinding<RecipeCard, Integer> servingsBinding;
+	SpinnerBinding<ScaleFactor, Integer> servingsToMakeBinding;
 	TextBinding<RecipeCard> descriptionBinding;
 	ObservableListener<CollatedDurationPanel, RecipeCard> durationListener;
 	ObservableListener<JPanel, RecipeCard> childListener;
 	
 	JTextField txtTitle;
 	JTextArea txaDescription;
+	JSpinner spnServings;
+	JSpinner spnServingsToMake;
 	
-	public SummaryPanel() {
+	public SummaryPanel(final ScaleFactor scale) {
 		controller = new ReplaceChildrenAction<>(RecipeCard::getElements, element -> {
 			JPanel wrapper = new JPanel(new VerticalLayout());
-			wrapper.add(new ElementPanel(element));
+			wrapper.add(new ElementPanel(element, scale));
 			wrapper.add(new JSeparator());
 			return wrapper;
 		});
@@ -106,12 +110,14 @@ public class SummaryPanel extends JPanel {
 		gbc_lblServes.gridy = 0;
 		panelServings.add(lblServes, gbc_lblServes);
 		
-		JSpinner spnServings = new JSpinner(new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1));
+		spnServings = new JSpinner(new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1));
+		spnServingsToMake = new JSpinner(new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1));
 		GridBagConstraints gbc_spnServings = new GridBagConstraints();
 		gbc_spnServings.fill = GridBagConstraints.HORIZONTAL;
 		gbc_spnServings.gridx = 1;
 		gbc_spnServings.gridy = 0;
 		panelServings.add(spnServings, gbc_spnServings);
+		panelServings.add(spnServingsToMake, gbc_spnServings);
 		
 		JPanel panelIngredients = new JPanel();
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
@@ -161,6 +167,13 @@ public class SummaryPanel extends JPanel {
 		servingsBinding = new SpinnerBinding<>(spnServings, 
 				RecipeCard::getServings, RecipeCard::setServings);
 
+		servingsToMakeBinding = new SpinnerBinding<>(spnServingsToMake, 
+				ScaleFactor::getOutputServings, (obj, s) -> {
+					obj.setOutputServings(s);
+					ObserverDispatch.notifySubscribers(obj);
+				});
+		servingsToMakeBinding.setModel(scale);
+
 		descriptionBinding = new TextBinding<>(txaDescription,
 				RecipeCard::getDescription, RecipeCard::setDescription);
 
@@ -172,8 +185,8 @@ public class SummaryPanel extends JPanel {
 				(c, v) -> controller.accept(c, v));
 	}
 	
-	public SummaryPanel(final RecipeCard card) {
-		this();
+	public SummaryPanel(final RecipeCard card, final ScaleFactor scale) {
+		this(scale);
 		setModel(card);
 	}
 	
@@ -185,6 +198,16 @@ public class SummaryPanel extends JPanel {
 		childListener.setObserved(card);
 	}
 	
+	@Override
+	public void setEnabled(boolean enabled) {
+		super.setEnabled(enabled);
+		txtTitle.setEditable(enabled);
+		txaDescription.setEditable(enabled);
+		spnServings.setVisible(enabled);
+		spnServingsToMake.setValue(spnServings.getValue());
+		spnServingsToMake.setVisible(!enabled);
+	}
+	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

+ 20 - 0
src/main/lombok/org/leumasjaffe/recipe/viewmodel/ScaleFactor.java

@@ -0,0 +1,20 @@
+package org.leumasjaffe.recipe.viewmodel;
+
+import org.leumasjaffe.observer.Observable;
+import org.leumasjaffe.recipe.model.RecipeCard;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE)
+public class ScaleFactor extends Observable.Instance {
+	@Setter RecipeCard model = null;
+	@Getter @Setter int outputServings;
+	
+	public double getScale() {
+		return (model == null || outputServings == 0) ? 1.0 :
+			outputServings / (double) model.getServings();
+	}
+}

+ 2 - 0
src/test/java/org/leumasjaffe/recipe/controller/AddPhaseActionTest.java

@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.view.PhasePanel;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.InjectMocks;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -19,6 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 class AddPhaseActionTest {
 	
 	@Spy Element model;
+	@Spy ScaleFactor scale;
 	@Spy JPanel view;
 	@InjectMocks AddPhaseAction action;
 	

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/ElementPanelTest.java

@@ -16,6 +16,7 @@ import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.leumasjaffe.recipe.model.Element;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
@@ -30,7 +31,7 @@ class ElementPanelTest extends SwingTestCase {
 	
 	@Mock ObservableListener<CollatedDurationPanel, Element> durationListener;
 	@Spy JPanel panelViewPort;
-	@InjectMocks ElementPanel panel = new ElementPanel();
+	@InjectMocks ElementPanel panel = new ElementPanel(new ScaleFactor());
 
 	@BeforeEach
 	void setUp() {

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

@@ -10,6 +10,7 @@ import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -22,7 +23,7 @@ class IngredientPanelTest extends SwingTestCase {
 	@BeforeEach
 	void setUp() {
 		stuff = new Ingredient("Onions", "Sliced", new Amount("100 g"));
-		panel = new IngredientPanel(stuff);
+		panel = new IngredientPanel(stuff, new ScaleFactor(), true);
 		listener.setObserved(stuff);
 		// setObserved invokes our callback.
 		clearInvocations(listener);

+ 0 - 51
src/test/java/org/leumasjaffe/recipe/view/IngredientPreparationPanelTest.java

@@ -1,51 +0,0 @@
-package org.leumasjaffe.recipe.view;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.leumasjaffe.observer.ObserverDispatch;
-import org.leumasjaffe.recipe.model.Amount;
-import org.leumasjaffe.recipe.model.Ingredient;
-
-class IngredientPreparationPanelTest extends SwingTestCase {
-
-	Ingredient stuff;
-	IngredientPreparationPanel panel;
-	
-	@BeforeEach
-	void setUp() {
-		stuff = new Ingredient("Onions", "Sliced", new Amount("100 g"));
-		panel = new IngredientPreparationPanel(stuff);
-	}
-
-	@Test
-	void testFilledOutWithContent() {
-		assertEquals("Onions", panel.getTxtName().getText());
-		assertEquals("Sliced", panel.getTxtPreparation().getText());
-		assertEquals(new Amount("100 g"), panel.getTxtAmount().getValue());
-	}
-	
-	@Test
-	void testCannotEditContent() {
-		assertFalse(panel.getTxtName().isEditable());
-		assertFalse(panel.getTxtPreparation().isEditable());
-		assertFalse(panel.getTxtAmount().isEditable());
-	}
-
-	@Test
-	void testIsSubscribedToUpdates() {
-		stuff.setName("Bacon");
-		stuff.setPreparation("Cut into Lardons");
-		stuff.setAmount(new Amount("50 g"));
-		
-		assertEquals("Onions", panel.getTxtName().getText());
-		
-		ObserverDispatch.notifySubscribers(stuff);
-		
-		assertEquals("Bacon", panel.getTxtName().getText());
-		assertEquals("Cut into Lardons", panel.getTxtPreparation().getText());
-		assertEquals(new Amount("50 g"), panel.getTxtAmount().getValue());
-	}
-
-}

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/PhasePanelIT.java

@@ -14,6 +14,7 @@ import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Step;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -31,7 +32,7 @@ class PhasePanelIT extends SwingTestCase {
 		steps = new ArrayList<>(Arrays.asList(step));
 		doReturn(steps).when(stuff).getCooking();
 				
-		panel = new PhasePanel(stuff);
+		panel = new PhasePanel(stuff, new ScaleFactor());
 		listener.setObserved(stuff);
 		// setObserved invokes our callback.
 		clearInvocations(listener);

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

@@ -14,6 +14,7 @@ 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.viewmodel.ScaleFactor;
 import org.mockito.Mock;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -35,7 +36,7 @@ class PhasePanelTest extends SwingTestCase {
 		doReturn(Arrays.asList(stub)).when(stuff).getCooking();
 		doReturn(prep).when(stuff).getPreparation();
 		doReturn(rest).when(stuff).getRest();
-		panel = new PhasePanel(stuff);
+		panel = new PhasePanel(stuff, new ScaleFactor());
 		
 		listener.setObserved(stuff);
 		// setObserved() calls update

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java

@@ -13,6 +13,7 @@ import org.leumasjaffe.recipe.controller.ReplaceChildrenAction;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Preparation;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -24,7 +25,7 @@ class PreparationPanelTest extends SwingTestCase {
 	@Mock ReplaceChildrenAction<Preparation, Ingredient> controller;
 	Preparation stuff = new Preparation();
 	@Mock Phase parent;
-	@InjectMocks PreparationPanel panel = new PreparationPanel();
+	@InjectMocks PreparationPanel panel = new PreparationPanel(new ScaleFactor());
 	
 	@BeforeEach
 	void setUp() {

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java

@@ -14,6 +14,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.summary.SummaryPanel;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
@@ -27,7 +28,7 @@ class RecipeCardPanelTest {
 
 	@Mock SummaryPanel summaryPanel;
 	@Spy AutoGrowPanel<ElementPanel, Element> panelElements =
-			new AutoGrowPanel<>(Element::new, ElementPanel::new);
+			new AutoGrowPanel<>(Element::new, e -> new ElementPanel(e, new ScaleFactor()));
 	
 	@InjectMocks RecipeCardPanel panel = new RecipeCardPanel();
 	

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java

@@ -15,6 +15,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -25,7 +26,7 @@ class StepPanelIT extends SwingTestCase {
 	List<Ingredient> ingredients;
 	Step stuff;
 	
-	StepPanel panel = new StepPanel();
+	StepPanel panel = new StepPanel(new ScaleFactor());
 
 	@BeforeEach
 	void setUp() {

+ 3 - 2
src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java

@@ -18,6 +18,7 @@ import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.InjectMocks;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -29,8 +30,8 @@ class StepPanelTest extends SwingTestCase {
 	
 	@Spy JLabel lblIndex;
 	@Spy AutoGrowPanel<IngredientPanel, Ingredient> panelIngredients =
-			new AutoGrowPanel<>(Ingredient::new, IngredientPanel::new);
-	@InjectMocks StepPanel panel = new StepPanel();
+			new AutoGrowPanel<>(Ingredient::new, ing -> new IngredientPanel(ing, new ScaleFactor(), true));
+	@InjectMocks StepPanel panel = new StepPanel(new ScaleFactor());
 
 	@BeforeEach
 	void setUp() {

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/FileMenuTest.java

@@ -1,4 +1,4 @@
-package org.leumasjaffe.recipe.view;
+package org.leumasjaffe.recipe.view.menu;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -13,6 +13,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.leumasjaffe.recipe.controller.FileController;
+import org.leumasjaffe.recipe.view.SwingTestCase;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/summary/ElementPanelTest.java

@@ -14,6 +14,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.view.SwingTestCase;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.leumasjaffe.recipe.model.Element;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -32,7 +33,7 @@ class ElementPanelTest extends SwingTestCase {
 				new Ingredient("Garlic", "", new Amount("2 tsp"))))
 		.when(stuff).getIngredients();
 		
-		panel = new ElementPanel(stuff);
+		panel = new ElementPanel(stuff, new ScaleFactor());
 	}
 
 	@Test

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

@@ -8,6 +8,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.view.SwingTestCase;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 class IngredientPanelTest extends SwingTestCase {
 
@@ -17,7 +18,7 @@ class IngredientPanelTest extends SwingTestCase {
 	@BeforeEach
 	void setUp() {
 		stuff = new Ingredient("Onions", "Sliced", new Amount("100 g"));
-		panel = new IngredientPanel(stuff);
+		panel = new IngredientPanel(stuff, new ScaleFactor());
 	}
 
 	@Test

+ 2 - 1
src/test/java/org/leumasjaffe/recipe/view/summary/SummaryPanelTest.java

@@ -8,6 +8,7 @@ import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenAction;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
+import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -17,7 +18,7 @@ class SummaryPanelTest {
 	
 	RecipeCard card = new RecipeCard();
 	@Mock ReplaceChildrenAction<RecipeCard, Element> controller;
-	@InjectMocks SummaryPanel panel = new SummaryPanel(card);
+	@InjectMocks SummaryPanel panel = new SummaryPanel(card, new ScaleFactor());
 
 	@Test
 	void testUpdateToCardInvokesController() {